Updated Oct 11, 202612 min read

tRPC vs Encore.ts in 2026

Comparing two approaches to end-to-end type safety

tRPC and Encore.ts both promise end-to-end type safety, but they're fundamentally different tools. tRPC is an API layer that eliminates the need for schemas by inferring types directly from your backend code. Encore.ts is a full backend framework where infrastructure is defined in code and types flow through your entire system.

The choice isn't always either/or. They solve different problems, and this comparison helps you understand when each approach fits.

Quick Comparison

AspecttRPCEncore.ts
What It IsType-safe API layerFull backend framework
Type SafetyClient-server type inferenceTypes for APIs, services, and infrastructure
Works WithAny HTTP framework (Express, Fastify, Next.js)Standalone framework
InfrastructureNone (bring your own)Built-in (databases, Pub/Sub, cron)
Primary Use CaseFull-stack TypeScript apps (frontend + backend)Backend systems and microservices
TransportHTTP, WebSocketsHTTP (with automatic service communication)
Client GenerationAutomatic (TypeScript inference)Automatic (generated clients)
Local DevelopmentStandard toolingAutomatic infrastructure provisioning
AI Agent CompatibilityManual infrastructure setupBuilt-in infrastructure awareness
Best ForNext.js apps, monoreposDistributed systems, cloud deployments

Understanding the Approaches

tRPC

tRPC removes the API boundary between frontend and backend in TypeScript projects. Instead of defining schemas or generating clients, you call backend procedures directly with full type inference:

// server/router.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ getUser: t.procedure .input(z.object({ id: z.string() })) .query(async ({ input }) => { return { id: input.id, name: 'John', email: 'john@example.com' }; }), createUser: t.procedure .input(z.object({ email: z.string().email(), name: z.string(), })) .mutation(async ({ input }) => { return { id: '1', ...input }; }), }); export type AppRouter = typeof appRouter;
// client/index.ts import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from '../server/router'; const trpc = createTRPCClient<AppRouter>({ links: [httpBatchLink({ url: 'http://localhost:3000/api/trpc' })], }); // Full type safety - IDE autocomplete, type errors if mismatched const user = await trpc.getUser.query({ id: '1' }); console.log(user.name); // TypeScript knows this is string

Types flow directly from your backend to your frontend without code generation or schema files.

Encore.ts

Encore.ts is a backend framework where types define your APIs and infrastructure:

users/users.ts
// users/api.ts import { api } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; const db = new SQLDatabase("users", { migrations: "./migrations" }); interface User { id: string; name: string; email: string; } interface GetUserRequest { id: string; } export const getUser = api( { method: "GET", path: "/users/:id", expose: true }, async ({ id }: GetUserRequest): Promise<User> => { const user = await db.queryRow<User>` SELECT id, name, email FROM users WHERE id = ${id} `; if (!user) throw new Error("User not found"); return user; } );
service.ts
// From another service import { users } from "~encore/clients"; // Type-safe call const user = await users.getUser({ id: "1" });

Encore generates type-safe clients for service-to-service communication and can also generate clients for external consumers.

Key Difference: tRPC connects your frontend directly to your backend in a monorepo. Encore connects services within a backend system and provides infrastructure automation.

Type Safety Approaches

tRPC

tRPC's type inference is its core feature:

// Define a procedure const appRouter = t.router({ greeting: t.procedure .input(z.object({ name: z.string() })) .query(({ input }) => { return `Hello, ${input.name}!`; }), }); // Client automatically knows: // - greeting.query takes { name: string } // - greeting.query returns string const message = await trpc.greeting.query({ name: 'World' });

The magic happens through TypeScript's type inference. Runtime type information doesn't cross the network, so it's purely a development-time feature. You use Zod for runtime validation.

Encore.ts

Encore validates types at runtime based on your TypeScript interfaces:

greeting/greeting.ts
interface GreetingRequest { name: string; } export const greeting = api( { method: "GET", path: "/greeting", expose: true }, async ({ name }: GreetingRequest): Promise<string> => { return `Hello, ${name}!`; } );

Invalid requests are rejected before your handler runs. The same types work for service-to-service calls:

service.ts
import { greetings } from "~encore/clients"; // Type-safe, validated at both ends const message = await greetings.greeting({ name: "World" });

Verdict: tRPC provides seamless type inference for frontend-backend communication. Encore provides type safety across services with runtime validation. Both eliminate the need for manual API schemas.

Integration with Existing Code

tRPC

tRPC integrates with existing HTTP frameworks:

// With Express import express from 'express'; import { createExpressMiddleware } from '@trpc/server/adapters/express'; import { appRouter } from './router'; const app = express(); app.use('/api/trpc', createExpressMiddleware({ router: appRouter })); app.listen(3000); // With Next.js (App Router) // app/api/trpc/[trpc]/route.ts import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; import { appRouter } from '@/server/router'; const handler = (req: Request) => fetchRequestHandler({ endpoint: '/api/trpc', req, router: appRouter, }); export { handler as GET, handler as POST };

tRPC fits into your existing stack. It's an API layer, not a framework replacement.

Encore.ts

Encore is a standalone framework:

service.ts
// users/encore.service.ts import { Service } from "encore.dev/service"; export default new Service("users"); // users/api.ts import { api } from "encore.dev/api"; export const getUser = api(/* ... */);

You build your entire backend with Encore. If you have existing services, you can call them via HTTP or gradually migrate.

Verdict: tRPC is easier to adopt incrementally. Encore requires more commitment but provides more features.

Frontend Integration

tRPC

tRPC shines in full-stack TypeScript apps, especially with React:

// With React Query integration import { trpc } from './utils/trpc'; function UserProfile({ userId }: { userId: string }) { const { data: user, isLoading } = trpc.getUser.useQuery({ id: userId }); if (isLoading) return <div>Loading...</div>; return ( <div> <h1>{user?.name}</h1> <p>{user?.email}</p> </div> ); }

The React Query integration provides caching, refetching, and optimistic updates with full type safety.

Encore.ts

Encore can generate clients for external consumers:

# Generate TypeScript client encore gen client my-app --lang=typescript --output=./client
import Client from './client'; const client = new Client({ baseURL: 'https://my-app.encr.app' }); const user = await client.users.getUser({ id: '1' });

For React integration, you'd use the generated client with React Query or similar:

import { useQuery } from '@tanstack/react-query'; import { client } from './api'; function UserProfile({ userId }: { userId: string }) { const { data: user } = useQuery({ queryKey: ['user', userId], queryFn: () => client.users.getUser({ id: userId }), }); return <div>{user?.name}</div>; }

Verdict: tRPC has tighter React integration out of the box. Encore requires an extra step to generate clients, but works with any frontend framework.

Infrastructure

tRPC

tRPC doesn't include infrastructure, so you bring your own:

// With Prisma import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); const appRouter = t.router({ getUser: t.procedure .input(z.object({ id: z.string() })) .query(async ({ input }) => { return prisma.user.findUnique({ where: { id: input.id } }); }), });

You set up databases, manage connections, and handle infrastructure separately.

Encore.ts

Encore provides infrastructure primitives:

import { api } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; import { Topic, Subscription } from "encore.dev/pubsub"; import { CronJob } from "encore.dev/cron"; const db = new SQLDatabase("app", { migrations: "./migrations" }); const userCreated = new Topic<{ userId: string }>("user-created", { deliveryGuarantee: "at-least-once", }); const _ = new CronJob("cleanup", { title: "Daily cleanup", every: "24h", endpoint: cleanupEndpoint, });

Local development automatically provisions databases and supporting infrastructure.

Verdict: tRPC is infrastructure-agnostic. Encore provides integrated infrastructure with automatic provisioning.

Service-to-Service Communication

tRPC

tRPC is primarily designed for client-server communication, not service-to-service:

// Calling another service requires manual setup const otherService = createTRPCClient<OtherServiceRouter>({ links: [httpBatchLink({ url: process.env.OTHER_SERVICE_URL })], }); const result = await otherService.someMethod.query({ /* ... */ });

You manage service URLs and handle the setup yourself.

Encore.ts

Service communication is a core feature:

orders/orders.ts
// users/api.ts export const getUser = api(/* ... */); // orders/api.ts import { users } from "~encore/clients"; export const createOrder = api( { method: "POST", path: "/orders", expose: true }, async (req: CreateOrderRequest) => { // Type-safe, automatic service discovery const user = await users.getUser({ id: req.userId }); return { orderId: '1', user }; } );

Services discover each other automatically. Calls are traced across service boundaries. The service catalog visualizes your architecture.

Encore service catalog

Verdict: Encore is designed for service-to-service communication. tRPC is designed for client-server communication.

Observability

tRPC

tRPC doesn't include observability:

// Manual logging const appRouter = t.router({ getUser: t.procedure .input(z.object({ id: z.string() })) .query(async ({ input }) => { console.log('Getting user:', input.id); // Add your own tracing, metrics, etc. return { /* ... */ }; }), });

You integrate logging, tracing, and metrics yourself.

Encore.ts

users/users.ts
import { api } from "encore.dev/api"; import log from "encore.dev/log"; export const getUser = api( { method: "GET", path: "/users/:id", expose: true }, async ({ id }: { id: string }) => { log.info("fetching user", { userId: id }); // Database queries and service calls are traced automatically return { /* ... */ }; } );

Distributed tracing works automatically across services and database calls.

Encore distributed tracing

Verdict: Encore provides built-in observability. tRPC requires external tools.

AI Code Generation

TypeScript's backend ecosystem has more competing options at every layer than almost any other language, and AI agents will use a different combination of database client, auth setup, and deployment adapter on every prompt. The problem isn't that there's no structure available, it's that there are too many valid structures to choose from.

tRPC

tRPC is in a similar position to NestJS here: the router/procedure pattern gives agents a consistent structure, so an agent asked to "add an order creation endpoint" will reliably produce a t.procedure.input(z.object(...)).mutation(...) call. Where things diverge is everything around the router: which database client to use (Prisma, Drizzle, raw SQL), how to set up the auth context (custom context functions, session middleware, NextAuth integration), where to deploy (Express adapter, Next.js API route, standalone server), and how to manage connection pools and environment variables. tRPC solves the API layer, but the infrastructure decisions still vary.

// An agent asked to "add an order creation endpoint" will consistently generate: // - A t.procedure with Zod input validation and .mutation() // // But the infrastructure decisions still vary: // - Prisma client or Drizzle or a raw pg query // - Custom createContext() with session lookup or NextAuth // - Express adapter or Next.js route handler or standalone // - Different environment variable patterns and connection setup

Encore.ts

With Encore, the project's API patterns, infrastructure declarations, and service structure are already defined. The agent reads the existing conventions and writes code that follows them. A prompt to "add an order creation endpoint" produces an api() function with typed request/response interfaces and a database call using the existing SQLDatabase declaration, not a new architecture.

orders/orders.ts
// The agent sees existing patterns and follows them: export const create = api( { expose: true, auth: true, method: "POST", path: "/orders" }, async (req: CreateOrderRequest): Promise<Order> => { // Business logic only — the agent doesn't reinvent the structure const order = await db.queryRow<Order>` INSERT INTO orders (customer_id, total) VALUES (${req.customerId}, ${req.total}) RETURNING id, customer_id as "customerId", total `; return order!; } );

Encore also provides an MCP server and editor rules (encore llm-rules init) that give agents access to database schemas, distributed traces, and service architecture. This means agents can generate queries that match your actual tables, debug with real request data, and verify their own work. Read more in How AI Agents Want to Write TypeScript.

Verdict: tRPC provides consistent procedure definitions through its router pattern, but the infrastructure decisions (database client, auth context, deployment target) still vary between prompts. Encore gives agents both the application conventions and the infrastructure patterns to follow, plus live system context through MCP, which leads to more consistent, deployable code.

How to Choose

Start at the top and follow the first question you can answer with a yes:

Question 1

Is your main goal end-to-end type safety between a Next.js frontend and its API routes?

Yes →
tRPCTypes flow from procedures to the frontend
Question 2

Do you want infrastructure provisioned from your code, without building a DevOps team internally?

Yes →
EncoreInfrastructure automation in your own AWS or GCP account
Question 3

Are you building several services that share databases, Pub/Sub, or cron jobs?

Yes →
EncoreType-safe service calls and built-in tracing
Question 4

Are you adding type-safe APIs to an existing project without restructuring it?

Yes →
tRPCDrops into an existing server
EncoreBackend systems with typed services and managed infrastructure

Encore is open source with 12,000+ GitHub stars. It provisions infrastructure in your own AWS or GCP account, or you can build Docker images and deploy anywhere.

Can You Use Both?

Yes. tRPC and Encore solve different problems:

  • Use Encore for your backend services with databases, Pub/Sub, and service communication
  • Use tRPC (or Encore's generated clients) for your frontend-to-backend API layer

For example, you might have:

  • Encore services handling your backend logic
  • A Next.js frontend using Encore's generated TypeScript client

Or:

  • tRPC for your Next.js API routes
  • External services (whether Encore or something else) for background processing

Getting Started

Try both:

New to tRPC? Start with What Is tRPC?, which also compares it with gRPC and REST. See also: Best TypeScript Backend Frameworks for more options, or NestJS vs Encore.ts if you're considering a more traditional framework.

Or install the CLI and create a starter app to see Encore's type-safe APIs in action:

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.


Have questions about choosing a framework? Join our Discord community where developers discuss architecture decisions daily.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders