APIs

REST vs GraphQL for a Small Product API

Two endpoints, one database table, and a payload that will never change: that is the whole API for a lot of small products. A settings screen, a list of items, a create form. Maybe a search box. The team building it is often one or two people, and the frontend is a single web app talking to a single backend. For that product, the REST versus GraphQL decision is not the architectural fork it is made out to be in conference talks. It is a question about how much machinery a small team wants to own.

The honest version: REST will get a small product to launch faster with less to learn and less to break, and GraphQL earns its keep only when a specific kind of pain shows up. That pain is real, but it is narrower than the ecosystem suggests.

What a small product API actually does all day

Picture a habit tracker with a few thousand users. The backend exposes a list of habits, a single habit with its recent check-ins, a create endpoint, an update endpoint, a delete endpoint. The web app fetches the list on load, fetches one habit when a detail panel opens, and posts a check-in when a button is tapped.

Count the round trips per screen: three, maybe four. The payload for the habit list is perhaps 4 KB of JSON. Nobody is on a 3G connection in a rural area wondering why the screen takes eleven seconds. The API is not the bottleneck, and it will not be for a long time.

This is the modal small product. It has one client, a modest schema, and a user base measured in thousands rather than millions. The API exists to move a small amount of structured data between a browser and a database. Both REST and GraphQL do that. The question is which one adds less friction to the specific team building it.

Why REST keeps winning the first month

A REST endpoint is a URL, an HTTP verb, and a JSON shape. A developer who has never built an API can read a tutorial for twenty minutes and ship a working endpoint by lunch. The mental model fits in a sentence: GET reads, POST creates, PATCH updates, DELETE removes. The tooling is everywhere because the pattern is older than most of the frameworks people use to implement it.

Debugging is equally plain. A failing request shows up in the browser network tab as a URL with a status code. Copy it into curl, change a header, get the same response. There is no query document to parse, no resolver chain to trace, no schema to consult before a person can even confirm the endpoint exists.

Caching comes almost free. HTTP caches understand GET requests and response headers. A CDN in front of a REST API can serve list responses for a minute or an hour without anyone writing a line of cache logic. GraphQL, by default, sends everything through POST, and POST responses are not cached by the infrastructure that already exists between the server and the user.

None of this is glamorous. It is the reason REST remains the default for small products: the boring parts are already solved.

The over-fetching problem that GraphQL was built to fix

GraphQL's founding complaint is legitimate. A mobile screen needs a user's name, avatar URL, and the count of unread messages. A REST endpoint built for the profile page returns the full user object: bio, address, preferences, timestamps, the works. The client downloads fields it will never render. On a slow connection or a large list, that waste compounds.

The second complaint is under-fetching. A single screen needs data from three endpoints, so the client fires three requests and stitches the results together. On a fast connection this is invisible. On a phone in an elevator, it is three chances to fail instead of one.

GraphQL answers both by letting the client declare exactly which fields it wants, in one request, against one schema. The server resolves each field from whatever source it needs. The client gets a response shaped like its screen. That is a genuine improvement, and it is why teams with complex, multi-source frontends adopted it early.

The catch is that both problems are proportional to the complexity of the client. A small product with one web app and three screens rarely suffers from either. The over-fetching is a few kilobytes. The under-fetching is two extra requests. Neither shows up in a support ticket.

The hidden bill: schema, resolvers, and N+1

GraphQL is not a query language bolted onto an existing API. It is a layer that sits between the client and the data sources, and that layer has to be written, maintained, and debugged. A schema has to be defined in SDL or code. Every field needs a resolver. Authorization has to be enforced per field, because a client can ask for any field the schema exposes, including ones the UI never shows.

Then comes the N+1 problem, which is the classic first-month surprise. A query asks for a list of posts and each post's author name. A naive resolver fetches the posts in one query, then fetches the author for each post in a separate query. Ten posts become eleven database queries. A hundred posts become a hundred and one. The fix is a batching layer like DataLoader, which is a real tool but also a real abstraction that a small team now owns.

Add the operational surface: query depth limits to prevent a client from asking for a deeply nested graph that melts the database, query cost analysis, persisted queries, and a caching strategy that replaces the HTTP caching the team gave up. Each of these is solvable. Together they are a second product the team maintains alongside the first one.

For a two-person team shipping a habit tracker, that is a poor trade. The API was supposed to be plumbing. GraphQL turns it into a subsystem.

When the client count crosses a line

The calculus flips when the number of clients grows. One web app is easy. A web app plus an iOS app plus an Android app plus a public API for partners is a different situation, because each client wants a different slice of the same data. With REST, that usually means either fat endpoints that return everything for everyone or a proliferation of endpoints tailored to each client. Both get ugly.

GraphQL lets all four clients hit one schema and ask for what each one needs. The iOS app requests five fields, the partner API requests twenty, and the backend team writes the resolver once. The schema becomes the contract, and adding a field is a non-breaking change that no client has to adopt immediately.

There is a threshold worth naming. Below roughly three distinct clients with meaningfully different data needs, REST's simplicity usually wins. Above it, the schema-first discipline of GraphQL starts paying for its own overhead. The exact number is less important than the pattern: complexity in the client layer is what GraphQL is designed to absorb.

A pragmatic middle path that avoids the rewrite

Teams often treat this as a binary, but a small product can hedge without much cost. Start with REST. Keep the endpoints small and the response shapes close to what the screens need. When a screen starts making three or four requests to assemble one view, add a single composite endpoint that returns exactly that view's data. This is sometimes called a backend-for-frontend pattern, and it captures most of GraphQL's under-fetching benefit with none of the schema machinery.

If a second and third client appear and the endpoint count starts climbing, that is the signal to evaluate GraphQL seriously. By then the team knows its data model, knows which queries are hot, and can design a schema around real usage instead of guesses. Migrating a small, well-understood API is a week of work, not a quarter.

The reverse migration, from GraphQL back to REST, is rarer and more painful, which is an argument for not reaching for GraphQL before the pain is real.

What the choice costs in a year of maintenance

Imagine two versions of the same product a year after launch. The REST version has twenty endpoints, a handful of composite routes for the busiest screens, and a cache header on the list responses. The team has not thought about the API layer in months. It works.

The GraphQL version has a schema file, a resolver directory, a DataLoader setup, a query complexity limiter, and a persisted-query pipeline. It also works, and it is more flexible. But someone on the team has to understand all of it, and when that person leaves, the next hire inherits a system with more moving parts than the product itself strictly required.

That is the real trade for a small product. GraphQL buys flexibility and pays for it in operational surface area. REST buys simplicity and pays for it in occasional awkwardness when a screen needs data from three places. For most small products, the awkwardness is cheaper than the surface area.

The exception is worth stating plainly. If the product's core value depends on a rich, interconnected data graph (a social feed, a project management tool with nested tasks and dependencies, an analytics dashboard querying many sources), GraphQL's flexibility stops being a luxury and becomes the reason the product is possible. In that case, build the schema first and treat it as the product's spine.

For everyone else, the first endpoint should be a GET, the response should be JSON, and the team should get back to building the thing users actually pay for.

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button