Fixed-Scope Work

An API Built to a Contract, With an End Date

This is one of the few things here we own end to end rather than staffing. A defined set of endpoints, an agreed auth model, documentation that matches the code, and a price fixed before anybody starts.

Node.js API code and endpoint definitions open on a developer screen
What Makes This Different

An API Is a Promise That Other People Build On

A user interface can be redesigned on a Tuesday and nobody outside the company notices. An API cannot. The moment a mobile app, a partner integration or another team depends on a field, the shape of your responses becomes a commitment, and the cost of getting it wrong is paid by everyone downstream for years.

That is why this is fixed-scope work rather than something to improvise while doing other things. We agree the resources, the auth model, the error shapes and the pagination behaviour in writing first, build against that, and hand over documentation that matches what the code actually does.

If instead you want an API built incrementally alongside a changing product, that is a job for a Node developer on your team, not a fixed-price project. Fixed scope and moving requirements produce change requests, and nobody enjoys those.

Scope

What the Project Covers, and What It Does Not

Both lists are written into the agreement before work starts, which is the entire point of buying it this way.

In the contract

Delivered as part of the project

  • The agreed resources and endpoints, with request and response schemas
  • Authentication and per-record authorisation, specified rather than assumed
  • Pagination, filtering and sorting behaviour, including the awkward edges
  • A consistent error format and a documented list of what each code means
  • An OpenAPI document that is checked against the implementation
  • A test suite that exercises the contract, not only the happy path
Not in the contract

A separate phase, quoted separately

  • Resources nobody mentioned until the build was underway
  • Client applications that consume the API, web or mobile
  • Rebuilding a data model that turns out to be the real problem
  • Ongoing operation, on-call cover and incident response afterwards
  • Cloud and infrastructure costs, which are billed to you directly
  • Indefinite changes of direction, which need a developer rather than a project
The First Decision

REST or GraphQL, Decided on Evidence

Both are reasonable. The choice is about how many differently shaped clients you have and how much operational machinery you want to own.

REST

The default, and usually the right one

Resources at predictable paths, verbs that mean what they say, and status codes that intermediaries already understand. Caching, rate limiting, logging and monitoring all work with no extra effort because every tool in the stack already speaks HTTP.

  • Every developer you will ever hire already knows it
  • A slow endpoint is trivially visible in any access log
  • Its weakness is real: clients fetch too much, or make three calls for one screen
GraphQL

Worth it when clients genuinely differ

One endpoint, a typed schema, and clients asking for exactly the fields they need. That is a real advantage when several consumers want different slices of the same graph and you would otherwise be adding an endpoint per screen forever.

  • Caching moves from free to a design problem you have to solve yourself
  • Rate limiting by request count is meaningless when one query can be enormous
  • Authorisation has to be enforced per field and per node, not per route

The honest heuristic: if you cannot name two consumers who want materially different shapes of the same data, GraphQL is buying you flexibility you will not use and handing you operational work you did not have. Choosing it because it is more modern is how teams end up rate limiting by query complexity eighteen months later.

Request Path

What Every Request Passes Through

Each layer has one job and fails in its own way. Most of the design work in an API project is deciding what belongs at which step.

Client sends a request
Edge: TLS, routing and rate limiting
Authentication: who is calling
Validation: is the input well formed
Authorisation: may this caller touch this record
Handler: the actual business rule
Data layer: queries, transactions, jobs
Response: shaped, paginated, or an error

Where each layer goes wrong

Rate limiting at the edge is the cheapest protection you will ever add and the one most often skipped until an integration partner writes a retry loop without a delay. Authentication answers who is calling and nothing more, which is why treating it as the whole security story is the most common serious flaw we find in an audit.

Validation before authorisation keeps malformed input out of code that assumes it is well formed, and it belongs in one place with a schema rather than scattered through handlers. Authorisation is the step that has to ask whether this caller may see this particular record; an endpoint that checks only that somebody is logged in will happily hand over another customer’s data when the identifier in the path is changed.

The handler should be boring by the time a request reaches it. If it is still deciding permissions and parsing input, the layers above are not doing their jobs. The data layer is where the slow query lives, and it is worth knowing which endpoints issue one query per item in a list before a customer discovers it for you.

Design Decisions

The Six Things Settled Before Any Code Is Written

These are the decisions that are expensive to reverse once somebody is depending on them.

Resource modelling

What the nouns are, how they relate, and which of them clients actually address. The usual mistake is exposing database tables directly, which welds your storage decisions to your public contract and makes both harder to change.

The other usual mistake is the opposite: an abstraction so general that every caller needs three requests and a diagram to do anything simple.

Versioning strategy

Decide before launch how a breaking change reaches consumers. A version in the path is blunt and obvious and works. A strict policy of only adding fields, never removing or repurposing them, is cleaner and requires discipline that outlasts the people who agreed to it.

Either is fine. Having no policy and discovering the need for one during an incident is not.

Authentication and authorisation

Short-lived access tokens with refresh for first-party clients; narrowly scoped credentials for service-to-service traffic. The half that matters more is authorisation, enforced per record rather than per route, and tested as deliberately as any feature.

If you already run an identity provider, we integrate with it rather than building a second login system for you to maintain.

Pagination and filtering

Every collection endpoint is paginated from the first day, including the ones that currently return eleven rows. Cursor pagination is stable when records are being inserted underneath the reader; offset pagination is simpler and silently skips or repeats rows on a busy table.

Filtering and sorting get an explicit allowed list. An open-ended filter syntax is a promise to support every future query shape forever, and an invitation to a table scan.

Rate limiting and abuse

Limits per credential rather than per address, because addresses are shared and credentials are not. Return the limit, what is left and when it resets in headers, so a competent client can back off instead of hammering and guessing.

Write down what happens when a legitimate partner exceeds a limit at three in the morning, because the answer decides whether it becomes an incident or an email.

Error shapes clients can handle

One consistent body for every failure: a stable machine-readable code, a human-readable message, and field-level detail for validation problems. Clients should never have to match on the text of a message, because that text will be edited by somebody who does not know it is load bearing.

Status codes that mean what they say, and a correlation identifier in the response so a support conversation can find the exact request in the logs.

Handover

Documentation and Tests Are the Deliverable Too

An API that only its author can use has not been delivered, whatever the endpoints do.

OpenAPI that cannot drift

The specification is generated from the code or verified against it in the test suite, so a response that stops matching its schema fails the build rather than confusing somebody in six months. Hand-maintained documentation is worse than none, because people believe it and build against it.

Alongside the generated document go the things a schema cannot express: what each error code means, the rate limits, how pagination behaves at the boundaries, and a worked example of the two or three flows most consumers implement first.

Tests against the contract

Integration tests that call the API the way a client would, against a real database rather than a set of mocks that agree with whatever the code currently does. The interesting cases are the unhappy ones: expired tokens, a caller reaching for somebody else’s record, a filter with nonsense in it, the second page of a list that changed underneath.

More on how we approach this at testing and automation. If the project is a rescue rather than a new build, the first phase is usually enough tests to change anything safely.

Everything is written in your repository, in your style, using the frameworks you already run — Express, NestJS or Fastify. We do not introduce a framework your team would then have to learn in order to maintain what we left behind.

Rescue Work

Fixing an API You Already Have

More common than a new build, and usually scoped after reading the code rather than from a description of it.

Every endpoint fails differently. Some return 200 with an error field, some return 500 for a validation problem, some return an HTML page. Normalising this is unglamorous and immediately makes every client simpler.
A list endpoint returns everything. Fine at launch, fatal at scale, and the fix has to be staged because existing clients assume they receive the whole set in one response.
Authorisation is implied by the interface. The screen only shows your own records, so nobody checked on the server. Changing an identifier in the path reveals somebody else’s data. This is the finding we report most often in a code audit.
The documentation describes a previous version. Usually a wiki page written at launch and never touched since. Replacing it with a generated document that fails the build when it drifts ends the problem permanently.
One endpoint issues a query per item. The response is correct and the database is on fire. Often a five-line fix once somebody has actually looked, which is what performance work starts with.
Nobody can change it safely. No tests, no staging environment that resembles production, and one person who remembers why a field is called what it is called. That is the first phase, before anything else is worth attempting.
FAQ

API Project Questions

Should we use REST or GraphQL?

REST unless you can name the specific reason not to. GraphQL earns its place when several different clients need different shapes of the same data and you are tired of building an endpoint per screen. The price is that caching, rate limiting and authorisation stop being solved by the HTTP layer and become your problem. If one web client and one mobile client consume the API, a handful of well-chosen REST endpoints is less work to build and much less work to operate.

Do we need versioning from day one?

You need a versioning decision from day one, which is not the same as a version number. Decide how a breaking change will reach consumers and write it down. While every consumer ships with you, you can change the contract and coordinate the deploy. The moment somebody you do not control depends on a field, you need either a version in the path or a strict policy of only ever adding, and that policy is far more discipline than it sounds.

What does a fixed-scope API project include?

The endpoints in the agreed contract, the authentication and authorisation model, pagination and filtering behaviour, the error shapes, an OpenAPI document that matches the implementation, and a test suite that runs against the contract. What it excludes is anything discovered later that was not in the contract. That becomes a second phase with its own price rather than an argument about what was implied.

Can you fix an existing API rather than build a new one?

Often that is the better project. The usual work is normalising inconsistent error responses, adding pagination to an endpoint that returns everything, enforcing authorisation checks that were assumed rather than implemented, and writing the documentation that never existed. We scope it after reading the code, because an API that is merely untidy is a very different job from one that is structurally wrong.

How do you handle authentication and authorisation?

By matching what you already run wherever that is sensible. For first-party web and mobile clients, short-lived access tokens with refresh, stored where the platform makes them hardest to steal. For service-to-service traffic, credentials with narrow scopes rather than one key that can do everything. The authorisation half matters more and is the half usually done badly: every endpoint has to answer whether this particular caller may touch this particular record, not merely whether somebody is logged in.

What does good API documentation actually look like?

An OpenAPI document generated from the code or verified against it in the test suite, so it cannot drift, plus the parts a specification cannot express: what each error code means, what the rate limits are, how pagination behaves at the edges, and a worked example of the two or three flows most clients will implement first. Hand-written documentation that nothing checks is worse than none, because people believe it.

Send Us the Endpoints, or the Mess

A rough list of resources is enough to scope a new build. For an existing API, read access to the repository is enough to tell you whether this is a tidy-up or a rebuild.