
REST vs. GraphQL: A Frontend Engineer’s Reality Check
A practical comparison of REST and GraphQL for frontend developers, featuring code examples and a custom API consumer build.
I see the debate pop up in dev communities every week: "Should I use REST or GraphQL for my next SaaS?"
Most answers are theoretical. They talk about "resources" vs. "graphs" or "flexible schemas." While true, that doesn't help you when you are staring at a blank VS Code window, trying to architect a frontend that doesn't choke on network requests.
As someone who builds micro-SaaS tools and AI agents, I don't have the luxury of dogma. I choose the tool that reduces friction. Sometimes that means the rigid predictability of REST; other times, it demands the granular precision of GraphQL.
Today, we aren't just talking theory. We are going to look at this from a builder's perspective. We will look at how to structure a data-fetching layer for a dashboard, implementing a consumer for both architectures, and decide which one actually offers the better Developer Experience (DX).
The Scenario: The "Dashboard Problem"
To make this comparison fair, we need a common scenario. Let's say we are building a Creator Analytics Dashboard. We need to fetch:
- The User's Profile (Name, Avatar).
- Their latest 5 Projects.
- The aggregate total of views across those projects.
This is the classic "N+1" problem or "Under-fetching" scenario that usually pushes developers toward GraphQL. Let's look at how the code differs.
Approach 1: The REST Implementation
REST (Representational State Transfer) relies on resources. In a standard setup, our data is likely normalized across different endpoints.
To get our dashboard data, we likely need to hit:
GET /api/v1/user/meGET /api/v1/user/{id}/projects?limit=5
Here is what the frontend implementation looks like using standard fetch. I usually wrap this in a typed service to keep components clean.
// rest-consumer.ts
interface User {
id: string;
name: string;
avatar: string;
}
interface Project {
id: string;
name: string;
views: number;
}
async function loadDashboardData() {
try {
// 1. Fetch User
const userRes = await fetch('https://api.system.com/v1/me');
const user: User = await userRes.json();
// 2. Fetch Projects (Waterfall request)
const projectsRes = await fetch(`https://api.system.com/v1/users/${user.id}/projects?limit=5`);
const projects: Project[] = await projectsRes.json();
// 3. Client-side aggregation
const totalViews = projects.reduce((acc, curr) => acc + curr.views, 0);
return { user, projects, totalViews };
} catch (error) {
console.error("Failed to load dashboard", error);
throw error;
}
}
The Practical Pain Points
- Waterfall Requests: Notice we couldn't fetch projects until we had the User ID. This introduces latency. We could parallelize this with
Promise.allif the endpoints were independent, but often they aren't. - Over-fetching: The
/projectsendpoint might return description, creation date, and metadata we don't need for this specific view. We are wasting bandwidth. - Loose Typing: Unless you are sharing Typescript interfaces via a monorepo or using tools like OpenAPI (Swagger) generators, the frontend has to "guess" the shape of the response.
Approach 2: The GraphQL Implementation
GraphQL flips the model. Instead of the server deciding what you get, the client asks for exactly what it needs.
Here, we hit a single endpoint: POST /graphql.
// graphql-consumer.ts
const DASHBOARD_QUERY = `
query GetDashboardData {
me {
id
name
avatar
projects(limit: 5) {
id
name
views
}
}
}
`;
async function loadDashboardData() {
try {
const response = await fetch('https://api.system.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: DASHBOARD_QUERY }),
});
const { data, errors } = await response.json();
if (errors) {
throw new Error(errors[0].message);
}
// Aggregation still happens on client, or we could ask the API to do it
const totalViews = data.me.projects.reduce((acc: any, curr: any) => acc + curr.views, 0);
return { user: data.me, projects: data.me.projects, totalViews };
} catch (error) {
console.error("GraphQL Error", error);
throw error;
}
}
The Practical Wins
- Single Round Trip: No waterfalls. One request gets everything.
- Precision: We don't get fields we didn't ask for. This is massive for mobile users on weak connections.
- Introspection: Tools like GraphQL Codegen can automatically generate TypeScript types from your query. If the backend changes, your build breaks immediately (which is a good thing).
The Build: Analyzing the Trade-offs
I've built systems with both. When I was building an automation platform recently, I had to choose. Here is the decision matrix I used, based on the "consumers" we just built above.
1. Caching Strategy
REST Wins. This is the elephant in the room. REST leverages the HTTP standard. If I hit GET /projects, the browser, CDNs, and proxies can cache that response based on headers. It's "free" caching.
With GraphQL, everything is a POST request. CDNs treat POST as non-cacheable. To cache GraphQL, you need heavy client-side libraries like Apollo Client or URQL that maintain a normalized cache in memory. It adds complexity and bundle size to your application.
2. Error Handling
REST Wins (Simplicity). In REST, a 404 means not found. A 500 means server error. Your code can simply check response.status.
In GraphQL, you often get a 200 OK status even if the request partially failed. The error is buried in an errors array in the JSON body. You have to write custom logic to parse these errors. It's flexible, but it's more work to set up.
3. Rapid Iteration & Data Shape
GraphQL Wins. This is where GraphQL shines for frontend devs. If the design changes and now we need to show the "Project Description" on the dashboard, in REST, I have to ask the backend engineer to update the serializer or endpoint. In GraphQL, I just add description to my query string. No backend changes required. This allows frontend teams to move significantly faster.
When to use which?
After building these consumers, here is my rule of thumb for modern development.
Choose REST if:
- The data structure is flat and simple. If you are just fetching a blog post, GraphQL is overkill.
- You rely heavily on HTTP caching. If you need edge caching for public data, REST is easier.
- You want zero-dependency implementations. A simple
fetchworks everywhere without parsing logic. - You are building a public API. It is easier for third-party developers to integrate with REST than to understand your specific Graph schema.
Choose GraphQL if:
- You have a "Dashboard" type app. Complex relational data, many nested entities, and a user-specific view (which can't be cached by CDNs anyway).
- You have multiple clients. Web, Mobile, and Public API all need different shapes of the same data.
- Bandwidth is a concern. You need to minimize payload size for mobile markets.
- You use TypeScript. The end-to-end type safety generated from a GraphQL schema is the best developer experience I've found to date.
Final Thoughts
There is no superior architecture, only superior fit. For my recent AI agent platform, I chose GraphQL for the internal dashboard because of the complex relationships between Agents, Tasks, and Logs. However, for the public-facing webhook receivers, I used REST for its raw speed and simplicity.
Don't be afraid to mix them. Use the right tool for the specific job within your stack.
Comments
Loading comments...