REST API

Learn about REST architecture style.


REST (Representational State Transfer) is an architecture style. A set of guidelines or rules that tells you how to build an API.

REST is based on resources and uses HTTP methods or verbs on top of to perform actions.

Example

Imagine walking to the library and instead of talking to a librarian, you use a simple machine with buttons.

Press 1) to get details of a book. Press 2) to add a book. Press 3) to remove a book.

The machine works the same way no matter what button pressed or book chose, you will expect the action on any book to have the expected action result.

Everything in REST API is treated like a resource, just like each book is a resource in the library. To interact with these resources, you use actions like GET, ADD, REMOVE. These are HTTP methods or verbs in REST.

REST consists of three parts:

  • Resource: The resources are plural nouns and used in the URL. For example: users, orders, comments.
GET https://mywebsite.com/users/26  # get user with id 26
GET /orders     # get all orders 
GET /comments   # get all comments 
  • Action: The actions are the verbs of internet (HTTP verbs) that acts on a resource. The ones most used are:
GET     # read resource
POST    # create new resource 
PUT     # update resource
PATCH   # update resource partially
DELETE  # delete resource
  • Return State: The client sends a request to a resource URL. The request travels to the server. The server handles the request, reading the database and other operations, then sends a response back. This response back is the return state that can be either: JSON, HTML, XML, pdf, and other formats.

LinkIconBuilding RESTful APIs

LinkIconResource Naming Convention

Following best practices in resource naming helps clients understand and interact with your API more effectively.

Guidelines for designing URI structures:

  • Use nouns, not verbs to name endpoints.
/users
/getUsers
  • Organize URIs to reflect resource hierarchy.

    For example, use /users/{userId}/orders/{orderId} to indicate that orders belong to users.

  • Use plural nouns for resources.

    For example, use /users for a collection of user resources. For a single user, use /users/{userId}

  • Use hyphens for multi-word URIs

    For example, use /user-profiles instead of /userProfiles.

  • Consistent naming conventions

    Stick to a single format: snake_case or camelCase, and use it consistently.

LinkIconFiltering, sorting, pagination

LinkIconFiltering

Filtering narrows a collection based on input values (called filters) typically found in the URL's query parameters.

Client request

GET /orders?status=paid&customerId=42

Backend example (Hono + Drizzle)

import { db } from '@/db'
import { orders } from '@/db/schema'
import { and, eq } from 'drizzle-orm'
 
app.get('/orders', async (c) => {
  const status = c.req.query('status')
  const customerId = c.req.query('customerId')
 
  const results = await db
    .select()
    .from(orders)
    .where(
      and(
        status ? eq(orders.status, status) : undefined,
        customerId ? eq(orders.customerId, Number(customerId)) : undefined,
      ),
    )
 
  return c.json(results)
})

LinkIconSorting

Sorting controls result order.

Use one parameter for the field and one for direction, or a single combined value.

Client request

GET /users?sort=createdAt:desc
# /users?sort={field}:{direction}

Backend example (Hono + Drizzle)

import { desc, asc } from 'drizzle-orm'
 
app.get('/users', async (c) => {
  const sort = c.req.query('sort') ?? 'createdAt:asc'
  const [field, direction] = sort.split(':')
 
  const orderBy =
    direction === 'desc'
      ? desc(users[field as keyof typeof users])
      : asc(users[field as keyof typeof users])
 
  const results = await db
    .select()
    .from(users)
    .orderBy(orderBy)
 
  return c.json(results)
})

LinkIconPagination

Pagination limits response size, preventing slow down on heavy loads.

There are two approaches: limit/offset and cursor-based.

LinkIconLimit and offset pagination

This approach is simple and familiar. It breaks when rows are inserted or deleted during pagination.

Client request

GET /products?limit=20&offset=40

Backend example (Drizzle)

app.get('/products', async (c) => {
  const limit = Number(c.req.query('limit') ?? 20)
  const offset = Number(c.req.query('offset') ?? 0)
 
  const results = await db
    .select()
    .from(products)
    .limit(limit)
    .offset(offset)
 
  return c.json(results)
})
LinkIconCursor-based pagination

Cursor pagination uses a stable reference point, usually an ID or timestamp. It survives deletes and inserts.

This is the correct default for production APIs.

Client request

First page:

GET /orders?limit=20

Next page:

GET /orders?limit=20&cursor=105

Backend example (Drizzle)

import { lt, desc } from 'drizzle-orm'
 
app.get('/orders', async (c) => {
  const limit = Number(c.req.query('limit') ?? 20)
  const cursor = c.req.query('cursor')
 
  const results = await db
    .select()
    .from(orders)
    .where(
      cursor ? lt(orders.id, Number(cursor)) : undefined,
    )
    .orderBy(desc(orders.id))
    .limit(limit + 1)
 
  const hasNextPage = results.length > limit
  const data = hasNextPage ? results.slice(0, limit) : results
 
  const nextCursor = hasNextPage
    ? data[data.length - 1].id
    : null
 
  return c.json({
    data,
    nextCursor,
  })
})

LinkIconVersioning

Versioning is essential because it ensures backward compatibility for clients. If your API is public and may change in the future, then you must version your API endpoints.

Here are some common methods for versioning APIs:

  1. URL Versioning

    Example: /v1/users
    Pros: Easy to understand and implement. Clear separation of versions.
    Cons: Can lead to URI clutter if many versions are maintained.

  2. Header Versioning

    Example: GET /users with Accept: application/vnd.example.v1+json
    Pros: Cleaner URLs. Versioning information is abstracted from the URI.
    Cons: Less visible and harder to test with simple tools like cURL.

  3. Query Parameter Versioning

    Example: /users?version=1
    Pros: Easy to implement and test.
    Cons: Clutters query parameters and mixes versioning with other query options.

My recommendation is go for #1.

LinkIconStatus Code

Use status code to know if a request failed or succeeded.

Don't inspect JSON body to know if a request failed.

Why? Status code tells you whether a response was successful or not. Status code lives in the header of a response, and for convenience, the header is one of the first bytes to arrive to the client. The JSON body arrives to the client later.

// Do
const response = await fetch("/users") // <- first bytes with success or fail response
response.ok // boolean - success or fail
response.status // number - status code
 
// Don't
const json = await response.json() // <- JSON body arrives after the response's header
json.error

The popular status codes are:

Number  Label — Description
200     OK — request succeeded
201     Created — resource created
204     No Content — success without response body
 
400     Bad Request — client sent invalid data
401     Unauthorized — missing or invalid auth
403     Forbidden — auth valid, access denied
404     Not Found — resource does not exist
409     Conflict — state conflict
422     Unprocessable Entity — validation failed
 
500     Internal Server Error — server bug

If clients must inspect JSON to know if a request failed, your API hides reality.

LinkIconStateless Requests

Every request contains all the information needed to handle it.

For example:

  • No server-side sessions tied to memory. No hidden context.
  • Authentication, locale, and permissions arrive with every request.

This constraint enables horizontal scaling without coordination.