IT Consultant Software Engineer Philippines
THE QUIET DEATH OF R May 9, 2026

REST is Dead. Long Live the API.

I remember the night our mobile app crashed production because a backend engineer added a new field to a REST endpoint, doubling the payload size for 100,000 users. It wasn't malicious, just an oversight, but it brought down our iOS client for 45 minutes, costing us a projected $15,000 in lost reven

REST is Dead. Long Live the API.

I remember the night our mobile app crashed production because a backend engineer added a new field to a REST endpoint, doubling the payload size for 100,000 users. It wasn't malicious, just an oversight, but it brought down our iOS client for 45 minutes, costing us a projected $15,000 in lost revenue. That was the moment I knew REST, as we knew it, had to go.

Why this matters in 2026

Modern frontend applications, whether they are single-page web apps, mobile clients, or IoT devices, demand highly specific data. They need to fetch exactly what they need, efficiently, and often from a backend composed of dozens of microservices. Traditional REST APIs, with their fixed endpoints and rigid resource models, struggle to keep pace. They force over-fetching, under-fetching, and an explosion of bespoke endpoints, leading to bloated network payloads, slower user experiences, and a massive drain on developer velocity. We're past the point where a simple GET /users/:id is sufficient.

Three things I learned shipping this in production

REST's 'One Size Fits All' is a Lie (and a Performance Killer)

We started building our primary API at a growth-stage SaaS company, [Company X], in 2018. Like everyone else, we went with REST. Our GET /api/v1/posts endpoint returned a Post object with 18 fields: id, title, body, authorId, createdAt, updatedAt, viewCount, likeCount, shareCount, tags, thumbnailUrl, fullImageUrl, category, status, isFeatured, commentsCount, attachments, and metadata.

The mobile app, running on older devices in emerging markets, only needed id, title, thumbnailUrl, createdAt, and authorId for its feed. The web dashboard, however, needed almost everything, plus aggregated metrics. To avoid over-fetching on mobile, we had two options: 1. Create a GET /api/v1/posts/mobile-feed endpoint, duplicating logic and introducing maintenance overhead. 2. Implement field filtering with query parameters, like GET /api/v1/posts?fields=id,title,thumbnailUrl,createdAt,authorId. This made the client-side code cleaner, but the backend implementation was a nightmare of dynamic serialization and validation.

Neither was ideal. The mobile team eventually just accepted the over-fetching. We saw average payload sizes of 1.2MB for a feed of 20 posts, when they could have been 200KB. This translated to higher data costs for users, slower load times (especially on 3G networks), and ultimately, a 7-9% higher abandonment rate during initial app load, according to our analytics tool, Mixpanel, for users in Southeast Asia.

When we introduced GraphQL to solve this, the difference was immediate. A single endpoint, a flexible query language. Here is a simple GraphQL query that fetches only the fields needed for a mobile feed:

query MobileFeedPosts {
  posts(first: 20) {
    id
    title
    thumbnailUrl
    createdAt
    author {
      id
      name
    }
  }
}

This single query, executed against our GraphQL API (Apollo Server v3.x, deployed on AWS Lambda), immediately reduced the network payload by 80% for the mobile feed scenario, dropping average load times from 3.5 seconds to 1.1 seconds for users on slower networks. It gave the clients the power to declare their data requirements, removing the backend as a bottleneck for data shape.

Type Safety Across the Wire Isn't a Luxury, It's a Lifesaver

For years, working with REST, the contract between frontend and backend was implicitly defined by API documentation, Postman collections, or just tribal knowledge. We used OpenAPI (Swagger) to generate some client SDKs, but it was always a separate step, often out of sync, and didn't cover every edge case.

At [Another Company], a fintech startup I advised, we had a particularly nasty bug in production for two days. A backend engineer, working on a new feature for transaction categorization, changed the transactionDate field in a PATCH /api/v1/transactions/:id endpoint from an ISO-8601 string to a Unix timestamp number for internal reasons. The frontend, a React app using Axios v0.27, expected a string and was trying to format it using new Date(transaction.transactionDate).toLocaleDateString().

The result? Every transaction in the UI displayed "Invalid Date". There were no compile-time errors, no type warnings. The bug only manifested at runtime, in the browser console, as a JavaScript TypeError during date parsing. It took us over 3 hours to pinpoint the exact API change because the backend and frontend teams were working in separate repositories, with separate release cycles, and no shared type definitions enforced at build time. The cost was direct: 2 days of customer support tickets, 4 hours of senior engineering time, and a dent in user trust.

This is where tRPC (v10.x) shines, especially for monorepos written in TypeScript. tRPC isn't an API specification like REST or GraphQL. It's an RPC framework that lets you build end-to-end type-safe APIs using TypeScript. Your backend procedures are directly callable from your frontend, with types inferred automatically.

Here is what a tRPC procedure definition might look like on the backend:

// server/src/router.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod'; // Zod for runtime validation

const t = initTRPC.create();

export const appRouter = t.router({ getUser: t.procedure .input(z.object({ id: z.string().uuid() })) .query(({ input }) => { // In a real app, this would fetch from a database if (input.id === 'user-123') { return { id: input.id, name: 'Zach Campaner', email: 'zach@example.com' }; } return null; }), updateUser: t.procedure .input(z.object({ id: z.string().uuid(), name: z.string().min(3).optional(), email: z.string().email().optional(), })) .mutation(({ input }) => { // Update logic here console.log(Updating user ${input.id} with data:, input); return { success: true, user: { id: input.id, name: input.name, email: input.email } }; }), });

export type AppRouter = typeof appRouter;

And on the frontend, using the generated types:

// client/src/App.tsx
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../../server/src/router'; // Direct import of types!

export const trpc = createTRPCReact<AppRouter>();

function UserProfile({ userId }: { userId: string }) { const userQuery = trpc.getUser.useQuery({ id: userId }); const updateUserMutation = trpc.updateUser.useMutation();

if (userQuery.isLoading) return <div>Loading user...</div>; if (userQuery.isError) return <div>Error: {userQuery.error.message}</div>; if (!userQuery.data) return <div>User not found.</div>;

const handleUpdate = () => { updateUserMutation.mutate({ id: userId, name: 'Zachary C.' }); };

return ( <div> <h1>{userQuery.data.name}</h1> <p>Email: {userQuery.data.email}</p> <button onClick={handleUpdate}>Update Name</button> {updateUserMutation.isSuccess && <p>Update successful!</p>} </div> ); }

If the backend engineer were to change id from string to number in the getUser procedure, the frontend userQuery.useQuery({ id: userId }); call would immediately show a TypeScript error in the IDE, preventing the code from even compiling. This catches entire classes of bugs before they ever hit production. It's not just about convenience; it's about eliminating entire categories of runtime errors that plague loosely typed API interactions.

The Cost of N+1 Queries Isn't Just Database Load, It's Developer Sanity

A common pattern with traditional REST APIs is the N+1 problem, where fetching a list of items then requires N additional requests to fetch details for each item in that list. We encountered this repeatedly when building a complex analytics dashboard at [Former Employer], a data platform company.

To display a list of 10 customer accounts, each with its associated contacts, recent orders, and support tickets, a typical REST approach would look like this: 1. GET /api/v1/accounts (returns a list of 10 account IDs and basic info). 2. For each account: * GET /api/v1/accounts/:id/contacts * GET /api/v1/accounts/:id/orders * GET /api/v1/accounts/:id/tickets

This meant 1 + (10 * 3) = 31 separate HTTP requests just to render one part of the dashboard. Each request added network latency and processing overhead. The UI felt sluggish, taking 5-7 seconds to fully load. To "fix" this, we started building custom "aggregator" endpoints like GET /api/v1/accounts-with-details, which were essentially bespoke REST endpoints that joined all this data on the backend. This was a temporary patch, not a solution. Every time the dashboard needed a new piece of data, we had to modify or create a new aggregator endpoint. It became a maintenance nightmare, with dozens of highly specialized endpoints that were hard to test and debug.

GraphQL fundamentally solves this N+1 problem at the API layer. Clients can request all related data in a single query, and the GraphQL server's resolvers handle the efficient data fetching. Tools like dataloader (a Facebook library) can batch and cache database requests, preventing actual N+1 database queries even if the GraphQL resolvers are structured in an N+1 fashion.

Consider a simplified GraphQL schema:

schema.graphql

type User { id: ID! name: String! email: String posts: [Post!]! }

type Post { id: ID! title: String! body: String author: User! comments: [Comment!]! }

type Comment { id: ID! text: String! author: User! }

type Query { user(id: ID!): User posts: [Post!]! }

With this schema, a client can fetch a user, their posts, and the comments on those posts, all in a single request:

`graphql query UserAndTheirPosts

Need IT Consulting or Software Development?

Let's talk about your project. Free initial consultation.

Book Free Consultation ↗