Technology·14 min read

React Server Actions vs tRPC vs API Routes: A Decision Guide

Three viable ways to move data between your frontend and backend in a Next.js app. Each has real tradeoffs in type safety, performance, and developer experience. Here is how to pick.

Nate Laquis

Nate Laquis

Founder & CEO

The Data Layer Problem Every Full-Stack Team Faces

You have a Next.js 15 app. You need to get data from your database to your UI and send mutations back to the server. Sounds simple enough, except now you have three fundamentally different approaches to choose from, and each one shapes your entire architecture.

React Server Actions let you write server-side functions and call them directly from client components. No API layer, no fetch calls, no route handlers. You mark a function with "use server" and invoke it from a form or a client-side event handler. The framework handles serialization, the network request, and revalidation.

tRPC gives you end-to-end type safety by generating a fully typed client from your server-side router definitions. You write your procedures once on the server, and every call site on the client gets autocomplete, type checking, and inference without code generation or schema files.

API routes (Next.js route handlers, Express, Hono, Fastify) are the classic approach. You define HTTP endpoints, your client sends fetch requests, and you parse the response. Simple, universal, and understood by every developer who has ever built a web application.

At Kanopy, we have shipped production apps using all three approaches. Some projects use one exclusively. Others mix two or even all three in the same codebase. The right choice depends on your team, your product, and how your application will evolve over time.

code on a monitor showing server-side TypeScript functions and API logic

How Each Approach Works at a High Level

Before comparing tradeoffs, you need a clear mental model of what actually happens when data moves between client and server in each approach.

React Server Actions

Server Actions are functions that execute on the server but can be called from client components. When you define a function with the "use server" directive, Next.js creates a hidden POST endpoint behind the scenes. The framework serializes your arguments, sends them over the wire, executes the function on the server, and returns the result. If the action is invoked from a form element, it works with progressive enhancement, meaning the form submits even if JavaScript fails to load.

The key insight: Server Actions eliminate the concept of an "API layer" for mutations. You do not define routes, write request handlers, or parse request bodies. You write a function and call it.

tRPC v11

tRPC works by defining a router on the server with typed input and output schemas (typically using Zod). The tRPC client on the frontend infers all the types from the server router at build time. There is no code generation step. You import the router type, create a typed client, and every procedure call is fully typed from input validation to response shape. tRPC v11 introduced a major overhaul with better streaming support, improved React Server Component integration, and a smaller client bundle.

Traditional API Routes

Whether you use Next.js route handlers, Hono, Express, or Fastify, the pattern is the same: define HTTP endpoints that accept requests and return responses. The client makes fetch calls (or uses a wrapper like Axios or ky) and parses the JSON response. Type safety, if you want it, requires extra work through shared type definitions, OpenAPI specs, or generated clients.

Each approach represents a different point on the abstraction spectrum. Server Actions are the most abstracted (you barely see the network). tRPC sits in the middle (you see the procedures but not the HTTP details). API routes are the least abstracted (you manage HTTP directly).

Type Safety: Where tRPC Dominates

Type safety is the single most important factor for developer productivity on a growing codebase. It determines how fast you can refactor, how many bugs your compiler catches before production, and how confident new team members feel making changes.

tRPC offers the best type safety story of the three. When you change a procedure's input schema on the server, every call site on the client immediately shows a TypeScript error. The return type is inferred automatically. If you rename a field in your database query, the type error propagates all the way to your React component. No code generation. No build step. Just inference. This feedback loop is instantaneous in your editor and catches entire categories of bugs that would otherwise reach production.

Server Actions provide good type safety within a single codebase. The function's TypeScript signature is respected when you call it from a client component. However, there are caveats. Server Action arguments must be serializable (no Dates, no Maps, no class instances), and the serialization boundary can introduce subtle type mismatches. The return type works correctly if you are careful, but the useActionState hook adds some type gymnastics that can trip up less experienced developers.

API routes have no built-in type safety across the network boundary. You define your handler on the server, and your client makes a fetch call that returns any by default. You can improve this by sharing TypeScript interfaces between client and server, but that only gives you a promise that the types match, not a guarantee. OpenAPI specs with generated clients (using tools like openapi-typescript) get you closer to tRPC-level safety, but the toolchain is heavier and the feedback loop is slower.

If your team is fully TypeScript and your product is a Next.js application where the frontend and backend share a monorepo, tRPC's type inference is a significant competitive advantage. You move faster and break fewer things.

Performance, Bundle Size, and Caching

The performance characteristics of each approach are more nuanced than most comparison articles suggest. Let me break down what actually matters.

developer writing optimized TypeScript code for a web application

Bundle Size

Server Actions add almost nothing to your client bundle. The function bodies stay on the server. The client only gets a reference ID and a small serialization helper. For mutation-heavy apps, this is a meaningful advantage.

tRPC v11 has made significant progress on bundle size. The core client is roughly 3-5 KB gzipped. The React Query integration adds more, but if you are already using TanStack Query (and you probably should be), that cost is shared. Still, tRPC will always add more client-side JavaScript than Server Actions because it needs a client runtime for procedure calls, batching, and error handling.

API routes with raw fetch add zero library overhead. If you use Axios or ky, you add a few KB. The client code is minimal and predictable.

Caching and Revalidation

This is where the approaches diverge sharply.

Server Actions integrate directly with Next.js caching primitives. Call revalidatePath or revalidateTag inside your action, and the framework automatically invalidates the relevant cached data. This tight integration means your UI updates instantly after a mutation without manual cache management. For apps using React Server Components heavily, this is the smoothest data flow you can get.

tRPC relies on TanStack Query for caching. This is a mature, battle-tested caching layer with fine-grained control over stale times, refetch intervals, optimistic updates, and cache invalidation. You get more control than Server Actions, but you also manage more complexity. The TanStack Query devtools are excellent for debugging cache behavior.

API routes can use any caching strategy: TanStack Query on the client, HTTP cache headers on the server, CDN caching, or Next.js fetch caching with tags. You have maximum flexibility but no defaults. You build the caching story yourself.

Latency

Server Actions and API routes both make a single HTTP request per operation. tRPC can batch multiple procedure calls into a single HTTP request, which reduces round trips when your component needs data from several sources simultaneously. In practice, this batching can shave 50-100ms off page loads that trigger multiple queries.

Error Handling and Auth Middleware

How you handle errors and enforce authentication tells you a lot about the maturity of each approach.

Error Handling

Server Actions have an awkward error story. If a Server Action throws, the error does not propagate cleanly to the calling component by default. You need to use useActionState to capture error states, or return a result object with a success/error discriminator. The progressive enhancement story (forms work without JS) means your error handling has to account for both JavaScript-enabled and JavaScript-disabled scenarios. In practice, most teams settle on a { success: boolean, error?: string, data?: T } return pattern.

tRPC has the best error handling of the three. Errors thrown in procedures are automatically typed and propagated to the client. You can define custom error codes, attach metadata, and handle specific error types in your components. The TanStack Query integration gives you isError, error, and retry states out of the box. tRPC's error formatter lets you shape errors globally, so your entire app has consistent error handling without boilerplate.

API routes require manual error handling. You set HTTP status codes, format error response bodies, and parse them on the client. It works, but every team ends up building their own error handling wrapper. Without discipline, error formats drift across endpoints.

Authentication Middleware

Server Actions can check authentication by calling your auth library (NextAuth, Clerk, Lucia) at the top of each action. There is no built-in middleware pattern, so you either check auth in every action manually or create a wrapper function like withAuth that handles the check and passes the session to your action logic.

tRPC handles this elegantly through its middleware system. You define an authProcedure that validates the session and injects the user into the context. Every procedure that extends authProcedure automatically requires authentication. The user object is typed in the procedure's context, so you get autocomplete and type safety on user properties. This is the cleanest auth pattern of the three.

API routes use standard middleware patterns. In Next.js, you can use middleware.ts for route-level auth checks. In Express or Hono, you use middleware functions. The pattern is well-understood and well-documented.

Testing Each Approach

Your ability to test your data layer determines how confidently you ship changes. Each approach demands a different testing strategy.

Server Actions are surprisingly difficult to test in isolation. Because they rely on Next.js server context (cookies, headers, cache revalidation), you cannot simply import and call them in a Vitest file. You need either integration tests that spin up a Next.js server, or you need to extract your business logic into pure functions and test those separately. The action itself becomes a thin wrapper that handles serialization and calls your business logic. This is a good pattern regardless, but Server Actions force you into it.

tRPC procedures are the easiest to unit test. You can create a caller from your router and invoke procedures directly in your test files without an HTTP server. Pass a mock context (with a fake user session, a test database connection), call the procedure, and assert on the result. The types flow through your tests, so type errors in your tests catch real bugs. This testing experience is one of tRPC's strongest selling points.

API routes can be tested at multiple levels. You can test route handlers directly by constructing Request objects and asserting on Response objects. You can run integration tests with supertest or similar libraries. If you use Express or Hono, the testing story is mature and well-documented. Next.js route handlers are slightly harder to test in isolation because they depend on the Next.js request/response objects.

For teams that prioritize testing (and you should), tRPC's testing ergonomics are a real advantage. The ability to test your entire API contract in fast unit tests, with full type safety, is something neither Server Actions nor API routes match easily.

startup team collaborating on full-stack TypeScript application architecture

When to Use Each (and How to Mix Them)

Here is the opinionated take after building production apps with all three approaches.

Use Server Actions For Mutations

Server Actions shine brightest for form submissions and simple mutations: creating a record, updating a profile, toggling a setting, deleting an item. The progressive enhancement story is valuable for forms. The tight integration with Next.js caching makes UI updates seamless. If your mutation is a straightforward "take this data and write it to the database," Server Actions are the fastest path from idea to production code.

Do not use Server Actions for complex queries, paginated lists, or data fetching that needs client-side caching. They were designed for mutations, and they work best when used that way.

Use tRPC for Full-Stack TypeScript Apps

If your team writes TypeScript on both ends and your frontend and backend live in the same monorepo, tRPC is the most productive choice for your primary data layer. The type inference across the network boundary eliminates an entire class of bugs. The middleware system handles auth cleanly. TanStack Query handles caching. The testing story is excellent.

tRPC is especially powerful for apps with complex data requirements: dashboards with many queries, real-time features, apps where multiple components fetch different slices of related data. The batching and caching features handle this complexity well.

Use API Routes for Public or External APIs

If you are building an API that will be consumed by mobile apps, third-party integrations, webhooks, or any client that is not your own React frontend, you need traditional API routes. tRPC and Server Actions are tightly coupled to your React frontend. They are not designed for external consumption.

API routes are also the right choice when you need fine-grained control over HTTP semantics: custom status codes, specific cache headers, content negotiation, streaming responses, or WebSocket upgrades. If you are building a REST or GraphQL API that follows standard HTTP conventions, route handlers are the appropriate tool.

Mixing Approaches in the Same App

This is not only acceptable, it is often the best strategy. A common pattern we use at Kanopy:

  • tRPC for the primary data layer between the React frontend and the backend
  • Server Actions for simple form mutations and actions that benefit from progressive enhancement
  • API routes (Hono or Next.js route handlers) for webhook endpoints, third-party integrations, and any public-facing API

The key is consistency within each category. Do not use Server Actions for some mutations and tRPC for others without a clear reason. Pick one as your default and use the others for specific, well-justified cases.

Decision Matrix: Team Size, App Type, and Scale

Use this matrix to make a fast, defensible decision for your project.

Solo Developer or Two-Person Team

Start with Server Actions for mutations and React Server Components for data fetching. You do not need the overhead of setting up tRPC when your team can see the entire codebase at a glance. Add tRPC later if your data layer grows complex enough to justify it. This is the fastest path to shipping.

Team of 3 to 8 Developers

tRPC becomes the right default here. With multiple developers making changes to both frontend and backend, the type safety across the network boundary prevents a constant stream of integration bugs. The time you save on debugging "why is this field undefined" pays for the setup cost within the first week. Use Server Actions alongside tRPC for progressive-enhancement forms.

Large Teams or Platform Products

If you have separate frontend and backend teams, or if your API serves multiple clients (web, mobile, third-party), traditional API routes with OpenAPI specs are the safest choice. The contract-first approach lets teams work independently. Code-generated clients give you type safety without requiring everyone to work in the same monorepo.

By App Type

  • SaaS dashboard: tRPC + Server Actions
  • E-commerce store: Server Actions + API routes (for payment webhooks and third-party integrations)
  • Content platform: Server Actions (mutations are simple, data fetching lives in Server Components)
  • Marketplace with mobile apps: API routes (you need a universal API) with tRPC for the web frontend
  • Internal tool: Server Actions. Keep it simple.

By Scale Requirements

All three approaches scale well technically. Server Actions and API routes run as standard HTTP handlers. tRPC procedures are just functions behind an HTTP adapter. The scaling differences are organizational, not technical. tRPC scales best when your entire team works in TypeScript. API routes scale best when your consumers are diverse. Server Actions scale best when your mutations are simple and your team is small.

The worst decision is no decision. Pick an approach, commit to it for your primary data layer, and revisit in six months with real usage data. The second worst decision is mixing everything without a clear convention, because that leads to a codebase where nobody knows which pattern to follow for the next feature.

If you are building a new product and want help choosing the right data layer architecture, or if you need a team that can ship your full-stack TypeScript app, book a free strategy call and let us figure it out together.

Need help building this?

Our team has launched 50+ products for startups and ambitious brands. Let's talk about your project.

React Server ActionstRPCAPI routesNext.jsfull-stack TypeScript

Ready to build your product?

Book a free 15-minute strategy call. No pitch, just clarity on your next steps.

Get Started