REST API design: the decisions that make an API pleasant to use
An API is a product with developers as users. Most of what makes one good is consistency, not cleverness.
GraphQL lets clients request exactly the fields they need from a typed schema through a single endpoint, which is valuable when many diverse clients consume one API. REST is simpler to cache, secure, monitor and operate, and remains the better default when one team owns both the API and its main client.
That last point is the real argument. If five client teams queue behind one backend team for endpoint changes, GraphQL removes the queue. If one team owns everything, the queue does not exist and you have paid for a solution to it anyway.
| Concern | REST | GraphQL |
|---|---|---|
| HTTP caching | Works natively per URL | Needs persisted queries or a client cache |
| Monitoring | Per-endpoint metrics | One endpoint; needs per-operation instrumentation |
| Rate limiting | Per endpoint | Must be cost-based on query complexity |
| Authorisation | Per endpoint | Per field, across every resolver path |
| Error handling | HTTP status codes | 200 with an errors array |
| N+1 queries | Explicit in your code | Emergent from resolver nesting |
Resolvers run per field per object, so fetching 50 posts and each post's author triggers 51 database queries unless requests are batched.
// Without batching: one query per post author
const resolvers = {
Post: { author: (post) => db.user.findUnique({ where: { id: post.authorId } }) },
};
// With DataLoader: one query for all authors in the tick
const userLoader = new DataLoader((ids) =>
db.user.findMany({ where: { id: { in: ids } } }).then((rows) =>
ids.map((id) => rows.find((r) => r.id === id) ?? null)
)
);
const resolvers = { Post: { author: (post) => userLoader.load(post.authorId) } };DataLoader-style batching is not an optimisation you add later. Any non-trivial GraphQL schema needs it from the start, plus per-request loader instances so caching does not leak across users.
There is also a middle path that has quietly won a lot of ground: typed server functions or tRPC-style RPC within a single TypeScript codebase. You get end-to-end type safety without a schema layer, provided both ends ship together.
Yes, and plenty of mature systems do — REST for public, cacheable, integration-facing endpoints and GraphQL for a rich internal client. The cost is two surfaces to secure, document and monitor, so it should be a deliberate decision rather than an accumulation.
Not inherently. It reduces round trips and over-fetching, which helps on high-latency mobile networks, but a badly written resolver tree can be far slower than a purpose-built REST endpoint.
Through a normalised client cache, persisted queries served over GET so a CDN can cache them, and server-side caching at the data layer. Standard URL-based HTTP caching does not apply to a single POST endpoint.
GraphQL returns 200 with an errors array by default, which surprises monitoring tools. Use typed error results in the schema for expected failures and make sure your observability surfaces them.
No. It remains the most widely understood, most cacheable and most operationally simple approach, and it is the right default for the majority of product APIs.
Harshal Patel
Founder & Lead Engineer, ROVQIX
Harshal leads engineering at ROVQIX, where he has shipped production Next.js, Node.js and PostgreSQL systems for startups, SaaS teams and ecommerce brands. He writes about the trade-offs behind architecture decisions rather than the framework of the week.
ROVQIXdesigns and builds production web platforms — Next.js front ends, Node.js APIs and the infrastructure behind them. Tell us what you're building and we'll scope it with you.
An API is a product with developers as users. Most of what makes one good is consistency, not cleverness.
The hard part of versioning is not the URL scheme. It is agreeing on what counts as breaking, and then actually retiring the old version.
Adding an index is easy. Knowing which one, in which column order, and which existing indexes to delete is the part that changes query times.
No spam. Just the occasional case study and craft breakdown.