REST APIs

Status: 🟩 COMPLETE Last updated: 2026-06-19 Plain-English tagline: The dominant style of web API — treat URLs as “things” (resources) and HTTP verbs as “actions” (GET to read, POST to create, PUT to update, DELETE to remove).


In plain English

REST stands for Representational State Transfer, a name only a computer scientist could love. The idea is much simpler than the name:

When you design an API, structure it around resources (the nouns your system manages) and use HTTP methods (the standard verbs the internet already speaks) to act on them. Forget custom verbs in URLs. Forget calling functions across the wire. Just URLs + methods.

Examples:

URLMethodMeans
/usersGETGet a list of all users
/users/123GETGet the user with id 123
/usersPOSTCreate a new user
/users/123PUTReplace the user with id 123
/users/123PATCHUpdate specific fields of user 123
/users/123DELETEDelete user 123
/users/123/postsGETGet all posts BY user 123
/users/123/postsPOSTCreate a new post BY user 123

The pattern is “URL identifies what; method says what to do with it.” Combined with JSON for payloads and standard HTTP status codes for outcomes, this convention forms the backbone of the modern web’s API layer.

Most APIs you’ll encounter — the GitHub API, Stripe API, Supabase’s auto-generated REST API, your own Next.js API routes — are REST-shaped, even when they don’t perfectly follow Roy Fielding’s original definition.


Why it matters

Three concrete payoffs of using REST conventions:

  1. It’s already familiar. Anyone who’s used the web has used REST without knowing it. New developers can read REST URLs and guess what endpoints do. Onboarding is fast.

  2. Tools assume it. Browsers, caches, CDNs, monitoring tools, security scanners — all designed around HTTP methods. GET is automatically cacheable; DELETE is automatically blocked from <a> tags. Following REST lets the entire HTTP ecosystem help you.

  3. It scales conceptually. Once you internalize “URL = resource, method = action,” every new API endpoint slots into a predictable pattern. Designers spend less time inventing URL schemes.

The trade-off: REST is loose. Two REST APIs can be “RESTful” and still feel completely different. There’s no enforced schema. The Stripe API and the GitHub API are both REST but have different conventions for pagination, errors, naming, auth. Every REST API has its quirks.


The HTTP methods, more carefully

Each HTTP method has documented semantics. Most APIs follow them, but some violate them. Knowing the intent helps you use methods correctly and spot misuse.

MethodPurposeSafe?Idempotent?Body?
GETRetrieve a resourceâś…âś…Usually no
HEADLike GET, but only return headersâś…âś…No
OPTIONSList allowed methods (CORS uses this)âś…âś…No
POSTCreate a resource OR perform an action❌❌Yes
PUTReplace a resource entirely❌✅Yes
PATCHPartially update a resource❌❌ (technically not required, but often is)Yes
DELETERemove a resource❌✅Optional

Safe = the method shouldn’t change state. Browsers may pre-fetch GET URLs for speed; they would not pre-fetch POST.

Idempotent = calling the method N times has the same effect as calling it once. DELETE /users/123 twice still ends with user 123 deleted. POST /users twice creates two users.

These properties matter for retries, caching, and CDN behavior. A non-idempotent operation should never be retried automatically without an idempotency key (see APIs overview).


Status codes — the response language

HTTP defines categories of status codes. REST APIs use them to communicate outcome:

RangeMeaningCommon codes
2xxSuccess200 OK, 201 Created, 204 No Content
3xxRedirection301 Moved Permanently, 302 Found, 304 Not Modified
4xxClient error (your fault)400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity, 429 Too Many Requests
5xxServer error (our fault)500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout

A well-designed REST API uses these precisely:

  • 200 OK — request succeeded, response body has the result.
  • 201 Created — POST succeeded; usually includes a Location header pointing to the new resource and the resource body.
  • 204 No Content — request succeeded, no body to return (common for DELETE).
  • 400 Bad Request — the request is malformed (bad JSON, missing field).
  • 401 Unauthorized — you haven’t authenticated (despite the name).
  • 403 Forbidden — you’re authenticated but not allowed.
  • 404 Not Found — the resource doesn’t exist.
  • 409 Conflict — request conflicts with current state (e.g. duplicate username).
  • 422 Unprocessable Entity — request was syntactically valid but semantically wrong (most validation errors).
  • 429 Too Many Requests — rate limit hit.
  • 500 Internal Server Error — unexpected backend bug.
  • 503 Service Unavailable — backend overloaded or down.

The line between 400 and 422 is fuzzy — many APIs use 400 for both. The line between 401 and 403 matters: don’t return 403 to anonymous users when you mean “log in first” — that’s 401.


A concrete example: a small REST API for blog posts

GET    /api/posts          → 200 [post, post, ...]
POST   /api/posts          → 201 { id, title, ... }
GET    /api/posts/47       → 200 { id, title, body, ... }   (404 if missing)
PATCH  /api/posts/47       → 200 { ...updated post }       (404 if missing)
DELETE /api/posts/47       → 204 (empty body)              (404 if missing)
GET    /api/posts/47/comments  → 200 [comment, comment, ...]
POST   /api/posts/47/comments  → 201 { id, body, ... }

In Next.js, that translates to a folder structure under app/api/posts/:

app/api/posts/route.ts            → GET (list) + POST (create)
app/api/posts/[id]/route.ts       → GET (one) + PATCH + DELETE
app/api/posts/[id]/comments/route.ts → GET (list) + POST (create)

Each route.ts exports functions named after HTTP methods:

// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
 
export async function GET(_req: NextRequest, { params }: { params: { id: string } }) {
  const post = await db.posts.findById(params.id);
  if (!post) return NextResponse.json({ error: "Not found" }, { status: 404 });
  return NextResponse.json(post);
}
 
const UpdatePostBody = z.object({
  title: z.string().min(1).optional(),
  body: z.string().optional(),
});
 
export async function PATCH(req: NextRequest, { params }: { params: { id: string } }) {
  const data = UpdatePostBody.parse(await req.json());
  const updated = await db.posts.update(params.id, data);
  return NextResponse.json(updated);
}
 
export async function DELETE(_req: NextRequest, { params }: { params: { id: string } }) {
  await db.posts.delete(params.id);
  return new NextResponse(null, { status: 204 });
}

Pattern: method → validate input → check auth → do the work → return appropriate status + body.


Naming conventions

Most well-designed REST APIs follow these:

  • Plural nouns for collections — /users not /user.
  • Lowercase, hyphenated URLs — /user-profiles not /UserProfiles or /user_profiles.
  • Hierarchy reflects ownership — /users/123/posts (posts that belong to user 123).
  • Query strings for filtering, sorting, pagination — /posts?status=published&sort=-created_at&page=2.
  • Avoid verbs in URLs — NOT /getUser/123 or /createUser. The verb is the HTTP method.

The verb-in-URL anti-pattern is the most common REST violation. POST /users/123/activate is technically not RESTful — but it’s pragmatic enough that nobody complains.


Pagination, filtering, sorting — the messy details

REST has no standard for these. Each API picks a style. The common patterns:

Pagination

StyleExampleProsCons
Page + size?page=2&size=20SimpleInconsistent when underlying data changes
Offset + limit?offset=20&limit=20SimpleSame problem as page+size
Cursor-based?cursor=abc123&limit=20Stable across data changesCan’t jump to a specific page
Link headersLink: <...?page=3>; rel="next"Self-discoverableLess obvious

For large or volatile collections, cursor-based pagination is the right answer. For small static lists, page+size is fine.

Filtering

?status=published&author_id=42&tag=javascript

Or, for complex queries, some APIs use a JSON body even on GET (unusual but seen) or a dedicated /search endpoint.

Sorting

?sort=created_at (ascending) or ?sort=-created_at (descending) — the leading hyphen for descending is a common convention.

Multiple: ?sort=-created_at,title.

?include=author,comments — expand certain relations inline (saves round trips). Or use sparse fieldsets: ?fields=title,body (return only specific columns).


Errors — give the caller something to work with

A bad REST error response: 500 { "error": "Something went wrong" }.

A good one:

{
  "error": {
    "code": "invalid_field",
    "message": "Title is required",
    "field": "title",
    "request_id": "req_abc123"
  }
}

The discipline: a stable code (machine-readable), a human message, the offending field if applicable, and a request_id so debugging is possible across logs.

For multi-error validation cases:

{
  "errors": [
    { "code": "required", "field": "title", "message": "Title is required" },
    { "code": "too_long", "field": "body", "message": "Body cannot exceed 10000 characters" }
  ]
}

The exact shape is a choice; consistency across your API is what matters.


Versioning a REST API

The classic options:

StrategyExampleNotes
URL versioning/v1/users, /v2/usersMost obvious, most disruptive
Header versioningAccept-Version: v2Cleaner URLs, hidden complexity
Query param/users?api-version=2Common in legacy/Azure APIs
Date-basedStripe-Version: 2024-01-01Best for evolving APIs (Stripe’s choice)
No versioning, backwards-compat additions/users forever, just add new fieldsHardest to do right, best UX when done right

For projects George is likely to build (and APIs he’ll consume), assume the rule is: never break a published endpoint. Add new fields, never remove or rename them. New behavior gets new endpoints or new optional parameters.


Caching — the underrated REST superpower

GET requests are designed to be cacheable. Browsers, CDNs, and reverse proxies all cache based on the URL + headers. Good REST APIs use this aggressively:

Cache-Control: public, max-age=300, s-maxage=600
ETag: "abc123"
Last-Modified: Wed, 19 Jun 2026 12:00:00 GMT

The next request can include If-None-Match: "abc123" or If-Modified-Since: ...; the server returns 304 Not Modified with no body, saving bandwidth.

For server-side data that changes rarely, this is a huge win. See CDNs for the broader caching layer.


REST vs RPC vs GraphQL — when to pick which

In broad strokes:

StyleBest for
RESTResource-oriented data, public APIs, mobile apps, sites that benefit from HTTP caching
GraphQLHeavily relational data, mobile apps wanting to minimize over-fetch, schemas you can introspect
gRPCBackend-to-backend communication, performance-critical paths, strongly-typed teams
tRPC / Server ActionsInternal first-party APIs within a TypeScript codebase — typed end-to-end

For first-party APIs in a Next.js project, the modern choice is often server actions or tRPC, not REST — because the typing reaches the client. For third-party-facing APIs (other developers will integrate), REST or GraphQL still dominate.


Common gotchas

  • PUT vs PATCH is easy to misuse. PUT is a full replacement: missing fields become null. PATCH updates only fields you send. Mixing them up is a classic bug.

  • POST for both create AND “do an action”. Strictly RESTful: create is POST to a collection (/users). But “run a calculation,” “send an email,” “approve an order” — these don’t map cleanly to CRUD. POST is the universal escape hatch. That’s pragmatic, but document it clearly.

  • 401 vs 403. 401 means “I don’t know who you are; authenticate.” 403 means “I know who you are; you can’t do this.” Many APIs confuse these.

  • 404 ambiguity. “User 123 doesn’t exist” vs “this endpoint doesn’t exist” are both 404. Surface enough error info to distinguish.

  • GET with side effects is a contract violation. A GET /users/123/delete endpoint is asking for trouble — browsers and crawlers might GET it speculatively. Side effects go in POST/PATCH/DELETE.

  • Verb in URL is a smell, not always wrong. POST /orders/47/cancel is technically un-REST but very common. The alternative (PATCH /orders/47 { "status": "cancelled" }) loses some intent. Pragmatism wins.

  • Inconsistent collection vs item endpoints. /users and /user/123 (singular/plural mix). Pick one convention.

  • Trailing slashes matter (sometimes). Some servers redirect /users/ to /users; others 404. Pick a convention and document it.

  • Content negotiation isn’t free. Allowing Accept: application/xml vs application/json doubles your test surface. Most modern APIs are JSON-only.

  • Date formats. ISO 8601 (2026-06-19T12:00:00Z) is the only sensible choice. Unix timestamps creep in occasionally; epoch milliseconds vs seconds is a common confusion.

  • Booleans in query strings are tricky. ?active=true, ?active=1, ?active=yes — depends on the framework. Be consistent.

  • Empty collections vs not-yet-existing collections. GET /users/123/posts → 200 with [] (user exists, has no posts) vs 404 (user 123 doesn’t exist). Both are valid; pick one.

  • Bulk operations don’t fit REST cleanly. Updating 100 records is awkward — PATCH /users with a body of changes? Each API has its own bulk convention; none are particularly clean.

  • HATEOAS is the part of REST nobody implements. Roy Fielding’s original REST included links between resources for navigation. Almost no modern REST API does this rigorously. The pragmatic version (1-2 link fields) is fine; full HATEOAS is rare.

  • Implicit type coercion in query params is a footgun. Everything in ?id=123&active=true is a string. Coerce explicitly.

  • Large request bodies need streaming. A 100MB upload via POST should stream, not be loaded into memory. Most frameworks handle this; verify yours does.

  • CORS preflights are real and surprising. Cross-origin POST with JSON content type triggers an OPTIONS preflight before the actual request. Your CORS middleware must handle both.

  • PUT is idempotent in theory; many APIs make it not. If PUT /users/123 auto-creates if missing (and that auto-creation includes generating an id internally), it’s no longer idempotent. Document carefully.

  • The “DELETE” status code question. Should DELETE return 204 (no body)? 200 (with the deleted resource as confirmation)? Both are used. Pick one.

  • Don’t mix REST styles within one API. If /posts uses page+size pagination, don’t make /comments use cursor-based. Consistency matters more than purity.

  • REST doesn’t enforce typing. A request POST /users { "age": "twenty" } reaches your handler; YOU validate it. Use Zod (or equivalent) at every entry point.

  • Mocking REST in tests is essential. Tools like MSW intercept fetch calls and return canned responses. Don’t hit the real API in unit tests.


See also


Sources