The Modern API Layer: GraphQL, REST, and tRPC
The debate is over. You need Type Safety. How to architect a scalable API layer that doesn't break the frontend when the backend sneezes.
The “Undefined is not a function” Problem
For 20 years, Frontend and Backend developers lived in silos.
Backend Dev: “I updated the User API. It returns fullName instead of first_name.”
Frontend Dev: “Okay.” (Forgets to update code).
Production: Crash. user.first_name is undefined.
This is called the Integration Gap.
Documentation (Swagger/OpenAPI) helps, but documentation lies. It gets outdated.
The modern solution is End-to-End Type Safety.
The code itself prevents these mismatch errors.
Why Maison Code Discusses This
At Maison Code, we inherit projects where the API layer is a mess of spaghetti endpoints.
/api/v1/get-user-final-final-2.
This slows down feature velocity to a crawl.
We implement Type-Safe Architectures.
We enable Frontend Autonomy. The frontend team should not have to bug the backend team to add one field to a JSON response.
We write about this because scalable teams run on scalable contracts.
If your schema is loose, your product is loose.
1. The Contenders
1. REST (with OpenAPI)
The classic.
- Pros: Simple, cacheable (HTTP 200), universal.
- Cons: Over-fetching (getting too much data) and Under-fetching (n+1 requests).
- Modern Twist: Use OpenAPI (Swagger) to generate TypeScript types automatically.
npx openapi-typescript schema.json -o schema.d.ts. Now, if the backend changes, the frontend build fails.
2. GraphQL
The power user.
- Pros: Client asks for exactly what it needs.
query { user { name, avatar(size: SMALL) } } - Cons: Complex caching (everything is POST 200), complexity (Resolvers, Dataloaders).
- Best For: Complex, graph-heavy apps (Social Networks, Dashboards) and Headless CMS integrations (Shopify, Contentful).
3. tRPC (TypeScript Remote Procedure Call)
The speed demon.
- Context: If you own both the Frontend and Backend (e.g., Next.js Monorepo).
- Magic: You import the backend function directly into the frontend code.
// Backend export const appRouter = router({ getUser: publicProcedure.query(() => { id: 1, name: 'Alex' }) }); // Frontend const user = trpc.getUser.useQuery(); console.log(user.data.name); // Typed! - There is no API schema. The code is the schema.
- Cons: Locked into TypeScript monorepos.
2. Pattern: The BFF (Backend for Frontend)
In Microservice architectures, you don’t want the frontend calling 10 distinct services (User Service, Cart Service, Product Service). It’s slow (10 round trips) and messy. Solution: A BFF Layer (usually GraphQL or Next.js API Routes). The Frontend calls the BFF once. The BFF calls the 10 services, aggregates the data, removes secrets, and sends back a clean JSON. This allows the Frontend to be “dumb” and the Backend to be “complex” without hurting performance.
3. Validating the Inputs: Zod
Never trust the client. Whether you use REST or GraphQL, you must validate inputs. Zod is the standard.
import { z } from 'zod';
const UserSchema = z.object({
email: z.string().email(),
age: z.number().min(18)
});
app.post('/user', (req, res) => {
const result = UserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json(result.error);
}
// Safe to proceed
});
This Zod schema can be shared with the frontend to generate Form Validation logic automatically (using react-hook-form).
4. The Federation Layer (Apollo)
For Enterprise apps, one GraphQL server is not enough. You have the “Product Team” in Berlin and the “Checkout Team” in New York. They cannot share one codebase. Solution: GraphQL Federation. Each team builds their own Subgraph. A “Gateway” stitches them together into one “Supergraph”. The Frontend queries the Gateway. It looks like one API, but it’s powered by 50 microservices. This is the architecture of Netflix and Airbnb.
5. Error Handling Strategies
“200 OK” but errors: ["Not Found"]. This is the GraphQL trap.
Strategy:
- Network Errors: (DNS, 500s). Retry with exponential backoff.
- Logic Errors: (User not found). Return a nullable type or a Union Type.
This forces the frontend to handle the error case explicitly in the code.union UserResult = User | UserNotFound | PermissionDeniedif (result.__typename === 'UserNotFound') ...Type-Safe Error Handling.
6. Caching Strategies (Stale-While-Revalidate)
REST caches easily via HTTP headers (Cache-Control: max-age=3600).
GraphQL is harder.
We use Stale-While-Revalidate (SWR) on the client (TanStack Query).
- Show cached data instantly (Stale).
- Fetch new data in background (Revalidate).
- Update UI if changed. This makes the app feel “instant”, even if the network is slow.
7. The Security Layer (Rate Limiting & JWT)
An API without security is an open door. Rate Limiting: Prevent DDoS.
- Use
upstash/ratelimit(Redis) on the Edge. - “10 requests per 10 seconds per IP”. Authentication:
- Stop using Cookies for APIs. Use JWT (JSON Web Tokens) in the
Authorization: Bearerheader. - Stateless. Scalable.
- But ensure you handle Token Rotation (Refresh Tokens) securely. Validation:
- Sanitize inputs against SQL Injection (use ORMs).
- Sanitize outputs against XSS.
8. Legacy Migration Integration (The Strangler Fig)
You have a monolithic Legacy API (Java/PHP). You want a modern Node.js API. Do not rewrite it all at once. You will fail. Use the Strangler Fig Pattern.
- Put a Proxy (Nginx / Cloudflare) in front.
- Route
/api/v1/usersto the New API. - Route everything else to the Legacy API.
- Slowly migrate endpoints one by one.
- Turn off the Legacy API when traffic drops to zero. This allows you to ship value immediately without a “Big Bang” rewrite.
9. Docs as Code (Stripe Standard)
How does Stripe keep their docs so good? They generate them from the code. Do not write API docs in Word or Confluence. Write them in the Schema.
- OpenAPI: Add
descriptionfields to your YAML. - GraphQL: Add
""" docstrings """to your schema. - Tools: Use Scalar or Redoc to render beautiful HTML docs from the schema.
- CI/CD: Deploy docs automatically on every merge. Outdated docs are worse than no docs.
10. The Skeptic’s View
“GraphQL is dead. Just use fetch.”
Counter-Point:
GraphQL is alive and well in the Enterprise.
Shopify, GitHub, and Facebook run on it.
“Just fetching” works for blogs.
It does not work for an E-commerce Product Page that needs Price, Variants, Inventory, Reviews, Recommendations, and User Wishlist status in one request.
If you do that with REST, you either have 6 requests (slow) or one monstrous /get-product-page-data endpoint (unmaintainable).
GraphQL solves the Orchestration problem.
11. Internal Traffic: Protocol Buffers (gRPC)
JSON is human-readable. It is also slow.
It repeats keys: {"name": "alex", "age": 10}. “name” is sent on the wire every time.
gRPC uses Protobuf (Binary).
It sends 0x12 0x04 0x61 0x6c 0x65 0x78.
It is 30% smaller and 5x faster to parse.
We use gRPC for internal service-to-service communication (Microservices).
We use GraphQL/JSON for the Client-to-Server communication.
Right tool for the right job.
12. The N+1 Problem (Data Loaders)
The classic GraphQL killer.
Query: users { posts { title } }.
- 1 Query for Users (SELECT * FROM users).
- 100 Queries for Posts (SELECT * FROM posts WHERE user_id = 1… 100).
- Total: 101 DB calls.
Solution: DataLoader.
It batches the 100 requests into ONE.
SELECT * FROM posts WHERE user_id IN (1, 2, ... 100). This reduces DB load by 99%. If you don’t use DataLoaders, your GraphQL server will melt.
13. Conclusion
The API layer is the nervous system of your application. If it is weak (untyped, untested), the body fails. If it is strong (tRPC, GraphQL Federation), the body moves with agility. Stop guessing properly names. Start enforcing them. Contract First. Code Second.
API breaking the Frontend?
We architect Type-Safe API layers using GraphQL and tRPC to ensure zero-integration bugs.