hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses REST API Design with @hectoday/http

What Makes an API RESTful

  • APIs are contracts
  • Project setup
  • Resources, not actions

HTTP Methods

  • GET, POST, PUT, PATCH, DELETE
  • Idempotency
  • Method safety and side effects

Status Codes

  • The status codes that matter
  • Error responses

Resource Design

  • Modeling resources
  • Partial responses and field selection
  • Pagination
  • Filtering, sorting, and searching

API Lifecycle

  • Versioning
  • Content negotiation
  • Rate limiting and quotas

Advanced Patterns

  • Bulk operations
  • Long-running operations
  • HATEOAS and discoverability

Putting It All Together

  • API design checklist
  • Summary

Method safety and side effects

The rule you can’t break

We’ve talked about idempotency: whether retries are safe. Now let’s talk about method safety: whether a request modifies data at all.

A method is safe if it doesn’t change anything on the server. GET and HEAD are safe. POST, PUT, PATCH, and DELETE are not. This sounds like a simple classification, but violating it can cause real damage.

Here’s why.

What goes wrong when GET modifies data

Imagine someone adds a convenient shortcut to their API:

GET /books/123/delete

It looks harmless. Visit the URL, the book gets deleted. Simple, right?

Now think about what happens in the real world.

Browsers prefetch links. Modern browsers sometimes load links before you click them, to make navigation feel faster. If the browser prefetches GET /books/123/delete, the book is gone before the user even clicked anything.

Search engine crawlers follow links. Google’s bot visits every link it finds on your page. If it discovers GET /books/123/delete, it follows it. Every book gets deleted. This has actually happened to real companies.

Proxies and CDNs cache GET responses. If your GET has side effects, the side effect only happens on the first uncached request. Everyone after that gets the cached response, and the side effect never fires again. The behavior becomes unpredictable.

The correct approach:

NEVER DO THIS:
GET /books/123/delete
GET /users/123/promote
GET /orders/123/ship

DO THIS:
DELETE /books/123
PATCH  /users/123  { "role": "admin" }
POST   /orders/123/ship

That last example is interesting. “Ship an order” isn’t a simple CRUD operation on a resource. It’s an action. POST is the right method for actions that don’t fit GET, PUT, PATCH, or DELETE. We’ll see this pattern more as we build out the API.

HEAD: checking without downloading

HEAD is identical to GET, but the server returns only the headers with no body. The client uses it to check if a resource exists or to read metadata without downloading the full response.

route.head("/books/:id", {
  request: { params: z.object({ id: z.string() }) },
  resolve: (c) => {
    if (!c.input.ok) return new Response(null, { status: 400 });
    const { id } = c.input.params;
    const book = books.find((b) => b.id === id);
    if (!book) return new Response(null, { status: 404 });
    return new Response(null, { status: 200 });
  },
});

You can test this with curl’s -I flag, which sends a HEAD request:

# Book exists: returns 200 with headers only, no body
curl -I http://localhost:3000/books/book-1

# Book doesn't exist: returns 404 with no body
curl -I http://localhost:3000/books/fake-id

When is this useful? A few scenarios: checking if a resource exists before downloading it, reading the Content-Length header to know how big a response will be, or verifying that a URL is valid without fetching the entire thing.

OPTIONS: what can I do here?

OPTIONS returns the methods a resource supports. Browsers send OPTIONS automatically before certain cross-origin requests. This is called a CORS preflight.

route.options("/books", {
  resolve: () => {
    return new Response(null, {
      headers: {
        allow: "GET, POST, OPTIONS",
        "access-control-allow-methods": "GET, POST, OPTIONS",
        "access-control-allow-headers": "Content-Type",
      },
    });
  },
});

In most apps, CORS headers are handled globally by your framework (Hectoday HTTP has CORS configuration for this). But understanding that OPTIONS is how the browser discovers allowed methods helps you debug cross-origin issues when they come up.

Caching and safe methods

Because GET is safe (it doesn’t modify data), its responses can be cached. This is a huge performance benefit. You can tell browsers and CDNs to cache a response so they don’t need to ask the server again:

route.get("/books/:id", {
  request: { params: z.object({ id: z.string() }) },
  resolve: (c) => {
    if (!c.input.ok) return Response.json({ error: c.input.issues }, { status: 400 });
    const { id } = c.input.params;
    const book = books.find((b) => b.id === id);
    if (!book) return Response.json({ error: "Not found" }, { status: 404 });

    return Response.json(book, {
      headers: {
        "cache-control": "public, max-age=60", // Cache for 60 seconds
      },
    });
  },
});

The Cache-Control header tells the browser: “You can reuse this response for the next 60 seconds without asking me again.” This works because GET is safe. The data doesn’t change as a result of reading it.

POST, PUT, PATCH, and DELETE responses should not be cached. They modify state, so the cached response would be stale immediately.

This is one of the real, practical benefits of using HTTP methods correctly. When GET means “read” and nothing else, the entire HTTP caching infrastructure works in your favor.

What’s next

We’ve covered HTTP methods thoroughly: what each one does, which are idempotent, which are safe, and the lesser-known HEAD and OPTIONS. Next, we’re moving on to what your server sends back: status codes. The difference between returning 200 and 201 and 204 matters more than you might think.

Exercises

Exercise 1: Add a HEAD /books/:id route. Test it with curl -I http://localhost:3000/books/book-1. You should see headers but no body.

Exercise 2: Add Cache-Control: public, max-age=60 to the GET /books response. Test with curl -v and observe the caching headers.

Exercise 3: Can you think of a case where a GET request legitimately needs to cause a change on the server? One common example is analytics tracking, like incrementing a view count. These are typically implemented as fire-and-forget side effects that don’t affect the response body.

Why is it dangerous to use GET for operations that modify data?

← Idempotency The status codes that matter →

© 2026 hectoday. All rights reserved.