Tag: backend

  • How to build a REST API with rate limiting

    How to build a REST API with rate limiting

    Any API exposed to the public will eventually get hammered, whether by a buggy client looping on an error, a scraper, or someone outright abusing it. Rate limiting is how you protect your service and keep it fair for everyone. I have added it to plenty of APIs, and the concepts are simpler than the jargon suggests.

    Pick an algorithm

    There are a few classic approaches and they trade off accuracy against cost. A fixed window counts requests per time bucket, say 100 per minute, and resets on the boundary. It is dead simple but allows bursts at the edges, since a client can fire 100 requests at the end of one minute and 100 at the start of the next. A sliding window smooths that out by weighting the previous window. A token bucket refills tokens at a steady rate and lets clients spend them in bursts up to a cap, which feels the most natural for real traffic.

    • Fixed window: easiest to build, allows edge bursts.
    • Sliding window: more accurate, slightly more bookkeeping.
    • Token bucket: handles bursts gracefully, my usual default.

    A simple token bucket

    The idea is that each client has a bucket of tokens. Every request costs one token, and tokens refill over time. If the bucket is empty, the request is rejected. Here is the core logic, ignoring storage for a moment:

    function allow(bucket, now, rate, capacity) {
      const elapsed = (now - bucket.last) / 1000;
      bucket.tokens = Math.min(capacity, bucket.tokens + elapsed * rate);
      bucket.last = now;
      if (bucket.tokens >= 1) {
        bucket.tokens -= 1;
        return true;
      }
      return false;
    }

    That function refills based on how much time has passed, caps the bucket so tokens do not accumulate forever, and spends one token per allowed request. It is maybe fifteen lines and it covers the vast majority of real needs.

    Identify the client correctly

    Rate limiting is only as good as your idea of who a client is. For authenticated traffic, key the limit on the API key or user ID, which is reliable. For anonymous traffic you fall back to IP address, which is imperfect because users behind the same network share an IP and proxies can spoof it. If you are behind a proxy or CDN, read the forwarded header, but only trust it when the request genuinely came through your infrastructure. Getting the key wrong means you either punish innocent users or fail to stop abusers.

    Respond the right way

    When you reject a request, do it with the correct HTTP status and helpful headers so well-behaved clients can adapt. The status is 429 Too Many Requests. Always include a Retry-After header telling the client how long to wait, and the standard rate limit headers so they can self-throttle before hitting the wall:

    HTTP/1.1 429 Too Many Requests
    Retry-After: 30
    RateLimit-Limit: 100
    RateLimit-Remaining: 0
    RateLimit-Reset: 30
    Content-Type: application/json
    
    { "error": "rate limit exceeded, retry in 30 seconds" }

    A good client reads those headers and backs off politely. A bad one ignores them, but at least you gave it the chance, and you have a clean record of why you said no.

    Where to store the counters

    An in-memory counter works for a single server and falls apart the moment you scale to two, because each instance has its own view. For anything distributed you need shared state. Redis is the classic choice; it is fast and has atomic increment operations that make this easy. On the edge, a key-value store like Cloudflare’s KV or Durable Objects does the same job close to the user. Whatever you pick, the counter update must be atomic, or two simultaneous requests can both read the same count and both get through.

    Layer it with other defenses

    Rate limiting is one layer, not the whole wall. Pair it with authentication, input validation, and sensible timeouts. Put it as early in the request lifecycle as you can, ideally before any expensive work, so a flood of blocked requests costs you almost nothing. If you are deploying this kind of service yourself, the same edge platform I describe in deploying to Cloudflare Pages can run the API and its rate-limit store together, and you can ship it confidently through a GitHub Actions pipeline. Build the limiter once, keep it boring, and it quietly does its job under load.

  • REST API design guidelines

    REST API design guidelines

    An API is a promise. Once a client depends on it, every quirk you shipped becomes permanent, because someone out there wrote code against that quirk. I have maintained APIs for years and the ones that aged well all shared the same trait: they were boring and predictable. Here is how I get there.

    Model resources as nouns, use verbs from HTTP

    The URL should name a thing. The method says what you are doing to it. I see endpoints like /getUser and /createOrderNow constantly, and they fight the whole point of HTTP. A clean design uses plural nouns for collections and lets the method carry the action.

    GET    /orders          list orders
    POST   /orders          create an order
    GET    /orders/42       fetch one order
    PATCH  /orders/42       update some fields
    DELETE /orders/42       remove it
    
    GET    /orders/42/items nested resource

    PATCH for partial updates and PUT for full replacement is a distinction worth keeping. Most real updates touch a few fields, so PATCH is what I reach for, and PUT becomes the rare case where the client genuinely owns the entire representation.

    Use status codes the way clients expect

    Return the status code that matches reality. A 200 on a failed request because “we put the error in the body” breaks every generic client and monitoring tool that reads the status line. The set I use covers almost everything:

    • 200 for a successful read or update, 201 when you created something.
    • 400 for malformed input, 422 when the input is well-formed but semantically invalid.
    • 401 when you do not know who they are, 403 when you know and they are not allowed.
    • 404 for a missing resource, 409 for a conflict like a duplicate.
    • 500 only for genuine server faults, never for a client mistake.

    Make errors machine-readable

    An error body should help the calling code react, not just print a string. I return a stable machine code alongside a human message, so clients can branch on the code without parsing prose that I might reword later.

    {
      "error": {
        "code": "insufficient_funds",
        "message": "The card was declined.",
        "field": "payment_method"
      }
    }

    Consistency here matters more than cleverness. Every error in the API should have the same shape, so a client writes one error handler instead of ten. This is the same instinct I bring to logging, which I wrote about in logging and observability best practices: structure beats prose when something else has to read it.

    Plan for versioning before you need it

    You will need to make a breaking change eventually. Decide how before you ship v1. I put the version in the path, /v1/orders, because it is visible, cacheable, and trivial to route. Header-based versioning is more elegant on paper and more annoying in practice when someone is debugging with curl. Whatever you pick, the rule is that you never break an existing version. Additive changes like new optional fields are fine. Removing a field or changing its type is a new version.

    Paginate and filter from day one

    Any collection that can grow will grow, and a GET that returns ten thousand rows will eventually time out and take a database with it. I add pagination to every list endpoint at the start, even when the data is tiny, because retrofitting it later is a breaking change. Cursor-based pagination handles large, shifting datasets better than offset, since offset drifts when rows are inserted mid-scroll.

    • Return a stable cursor and a clear “has more” signal.
    • Allow filtering with query parameters, and document exactly which fields are filterable.
    • Cap the page size server-side so a client cannot ask for everything at once.

    Be strict about what you accept, generous about what you return

    Validate input hard at the boundary and reject anything malformed with a clear 400 or 422. The closer to the edge you catch bad data, the less it can corrupt downstream, which ties straight back to the constraints I rely on in database schema best practices. On the output side, keep responses stable and predictable so clients can trust the shape. An API that is strict at the door and consistent on the way out is one people enjoy building against, and that good will is what gets your platform adopted.