REST API vs GraphQL vs tRPC: Which Should You Use in 2026?

Choosing an API style is one of the first architectural decisions in a new project, and the wrong choice creates friction for years. REST, GraphQL, and tRPC each solve real problems — but they solve different problems. This guide gives you a clear, practical comparison so you can make the right call for your specific situation.

REST: The Standard

REST (Representational State Transfer) is an architectural style built around HTTP verbs and resource URLs. Every resource has a URL; you interact with it using GET, POST, PUT, PATCH, and DELETE.

# REST endpoints for a blog
GET    /posts            # list all posts
GET    /posts/42         # get one post
POST   /posts            # create a post
PUT    /posts/42         # replace a post
PATCH  /posts/42         # partial update
DELETE /posts/42         # delete a post
GET    /posts/42/comments  # nested resource

A Simple REST API in Node.js (Express)

import express from 'express'
import { prisma } from './lib/prisma'

const app = express()
app.use(express.json())

// GET /posts
app.get('/posts', async (req, res) => {
    const posts = await prisma.post.findMany({
        where:   { published: true },
        include: { author: { select: { name: true } } },
        orderBy: { createdAt: 'desc' },
    })
    res.json(posts)
})

// POST /posts
app.post('/posts', async (req, res) => {
    const { title, content, authorId } = req.body
    const post = await prisma.post.create({
        data: { title, content, authorId, published: false },
    })
    res.status(201).json(post)
})

app.listen(3000)

REST Strengths

  • Universal support — every language, framework, and tool speaks HTTP/REST
  • HTTP caching — GET responses are cacheable by browsers, CDNs, and proxies out of the box
  • Stateless — each request is self-contained; easy to scale horizontally
  • Familiar to everyone — no learning curve for experienced developers
  • Easy to test — curl, Postman, browser DevTools all work natively

REST Weaknesses

  • Over-fetchingGET /users returns all fields even if you only need name
  • Under-fetching — displaying a post with author and comments requires 3 requests: /posts/1, /users/5, /posts/1/comments
  • No contract enforcement — clients and servers drift apart; the only contract is documentation
  • Versioning pain/api/v1/, /api/v2/ sprawl as the API evolves

GraphQL: The Flexible Query Language

GraphQL is a query language for APIs. Instead of fixed endpoints, you have a single endpoint (POST /graphql) and clients declare exactly what data they need.

# Client sends this query
query GetPost($id: ID!) {
    post(id: $id) {
        title
        content
        author {
            name
            avatar
        }
        comments(first: 5) {
            text
            createdAt
        }
    }
}

The server returns exactly that shape — no more, no less. One request replaces three REST calls, and there's no over-fetching.

GraphQL Server (Apollo with Prisma)

import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
import { prisma } from './lib/prisma'

const typeDefs = `#graphql
    type Post {
        id:       ID!
        title:    String!
        content:  String
        author:   User!
        comments: [Comment!]!
    }
    type User  { id: ID!; name: String! }
    type Comment { id: ID!; text: String! }

    type Query {
        post(id: ID!): Post
        posts(published: Boolean): [Post!]!
    }

    type Mutation {
        createPost(title: String!, content: String, authorId: ID!): Post!
    }
`

const resolvers = {
    Query: {
        post:  (_, { id }) => prisma.post.findUnique({ where: { id: Number(id) } }),
        posts: (_, { published }) => prisma.post.findMany({ where: { published } }),
    },
    Post: {
        author:   (post) => prisma.user.findUnique({ where: { id: post.authorId } }),
        comments: (post) => prisma.comment.findMany({ where: { postId: post.id } }),
    },
    Mutation: {
        createPost: (_, args) => prisma.post.create({ data: args }),
    },
}

const server = new ApolloServer({ typeDefs, resolvers })
await startStandaloneServer(server, { listen: { port: 4000 } })

GraphQL Strengths

  • Precise data fetching — clients get exactly what they ask for
  • Single request for complex data — fetch nested resources in one round trip
  • Strongly typed schema — the schema IS the contract; both client and server are bound to it
  • Introspection — tools like GraphiQL auto-generate documentation from the schema
  • Evolve without versioning — add fields freely; deprecate old ones with @deprecated

GraphQL Weaknesses

  • N+1 problem — fetching 100 posts with their authors triggers 101 DB queries without DataLoader
  • Caching is hard — HTTP caching doesn't work on a single POST endpoint; needs client-side caching (Apollo Client, urql)
  • Complexity overhead — schema definition, resolvers, and a client library for a simple CRUD app is overkill
  • File uploads are awkward — requires multipart spec; REST handles uploads natively

tRPC: Type-Safe APIs Without a Schema

tRPC takes a completely different approach: it generates a type-safe client directly from your server's TypeScript types. There's no schema language, no code generation step, no documentation to maintain — the server IS the contract.

// server/router.ts
import { router, publicProcedure } from './trpc'
import { z } from 'zod'
import { prisma } from './lib/prisma'

export const postRouter = router({
    list: publicProcedure
        .input(z.object({ published: z.boolean().optional() }))
        .query(({ input }) => {
            return prisma.post.findMany({ where: input })
        }),

    byId: publicProcedure
        .input(z.object({ id: z.number() }))
        .query(({ input }) => {
            return prisma.post.findUnique({ where: { id: input.id } })
        }),

    create: publicProcedure
        .input(z.object({ title: z.string().min(1), content: z.string().optional() }))
        .mutation(({ input, ctx }) => {
            return prisma.post.create({
                data: { ...input, authorId: ctx.user.id },
            })
        }),
})
// client/posts.tsx — full type safety, zero schema writing
import { trpc } from './trpc'

function Posts() {
    const { data: posts } = trpc.post.list.useQuery({ published: true })
    const createPost = trpc.post.create.useMutation()

    //                              ↑ TypeScript knows exactly what this returns
    //                                No type assertion, no manual interface

    return (
        <>
            {posts?.map(post => <div key={post.id}>{post.title}</div>)}
            <button onClick={() => createPost.mutate({ title: 'New Post' })}>
                Create
            </button>
        </>
    )
}

tRPC Strengths

  • Zero type drift — change a return type on the server; TypeScript errors appear in the client immediately
  • No code generation — no schema compilation, no graphql-codegen, no OpenAPI spec
  • First-class React Query integrationuseQuery and useMutation hooks built in
  • Input validation with Zod — automatic runtime validation + TypeScript type inference from the same schema
  • Minimal boilerplate — write a procedure, call it from the client; that's it

tRPC Weaknesses

  • TypeScript only — tRPC is useless without TypeScript on both client and server; a React Native app can use it but an Android/iOS native app cannot
  • Monorepo-optimized — works best when client and server share a codebase. Cross-repo or multi-team setups lose the type-sharing benefit
  • Not a public API — you can't expose tRPC as a third-party API; it's internal machinery, not a documented contract
  • Limited ecosystem — far fewer tools, middleware, and tutorials compared to REST or GraphQL

Head-to-Head Comparison

Caching

  • REST: Native HTTP caching — GET requests are cached by browsers and CDNs automatically
  • GraphQL: HTTP caching doesn't apply to POST; use Apollo Client's normalized cache or persisted queries
  • tRPC: GET requests for queries (default), so HTTP caching works; React Query handles client-side caching

Third-Party Consumers

  • REST: Any client in any language — the gold standard for public APIs
  • GraphQL: Any client with a GraphQL library; the schema is self-documenting
  • tRPC: TypeScript only — not suitable for external/public APIs

Team Size and Structure

  • REST: Works for any team structure; frontend and backend can be fully decoupled
  • GraphQL: Scales well when frontend teams need data autonomy — they write their own queries
  • tRPC: Best for small full-stack teams or a single developer owning the entire stack

Which to Choose?

Choose REST if:

  • You're building a public API consumed by third parties
  • Your clients are diverse (mobile apps, external partners, IoT devices)
  • Caching at the CDN layer is critical to performance
  • Your team includes developers not using TypeScript

Choose GraphQL if:

  • You have multiple frontend clients (web, mobile) that each need different data shapes
  • Frontend teams need to iterate on data requirements without backend changes
  • You're dealing with deeply nested, interconnected data (social graph, product catalog)
  • You need fine-grained permissions at the field level

Choose tRPC if:

  • You're building a full-stack TypeScript app (Next.js, Remix)
  • Client and server live in the same repo
  • The API is internal — never exposed to third parties
  • You want the fastest path to a type-safe backend without a GraphQL schema

Summary

REST is the default for public APIs and anything polyglot. GraphQL shines when you have multiple clients with divergent data needs. tRPC is the best developer experience for TypeScript full-stack apps where the API is internal. These aren't mutually exclusive either — a large product might use tRPC for its internal dashboard, REST for its public API, and GraphQL for its mobile apps, all from the same backend.

Frequently Asked Questions

Can I use tRPC with Next.js?

Yes — tRPC was designed with Next.js in mind. The @trpc/next adapter integrates with both the Pages Router and App Router. With App Router, you can use tRPC with React Server Components via the server-side caller. The T3 Stack (create.t3.gg) combines Next.js + tRPC + Prisma + Tailwind and is one of the most popular full-stack TypeScript setups in 2026.

Does GraphQL replace REST entirely?

No — and the idea that it would turned out to be wrong. GraphQL is better than REST in specific scenarios (multiple clients, complex nested data, frontend data autonomy). But REST's simplicity, universal tooling support, and native HTTP caching make it the right choice for most public APIs and simple CRUD backends. Many companies run both: a REST API for external partners and a GraphQL API for their own web and mobile frontends.

What is the N+1 problem in GraphQL and how do you fix it?

When you fetch a list of 100 posts and each post resolver fetches its author individually, you make 1 query for posts + 100 queries for authors = 101 total. This is the N+1 problem. The fix is DataLoader — it batches all the author lookups into a single SELECT * FROM users WHERE id IN (1, 2, 3...) query. Install dataloader and create one per request in your GraphQL context. Prisma also helps: include: { author: true } uses a JOIN instead of separate queries.

What is Zod and why does tRPC use it?

Zod is a TypeScript-first schema validation library. You define a schema like z.object({ email: z.string().email(), age: z.number().min(0) }) and Zod both validates input at runtime AND infers the TypeScript type from the schema — so you write the validation once and get type safety for free. tRPC uses Zod for procedure inputs because it solves two problems at once: runtime validation of incoming data (security) and TypeScript type inference (developer experience).