Skip to main content

gRPC vs Connect-RPC vs tRPC 2026

·APIScout Team
grpcconnect-rpctrpcrpcprotobuftypescript apiapi designmicroservices

REST Is Not the Only Option Anymore

For a decade, REST was the default API design pattern. In 2026, developers have real alternatives for specific use cases: gRPC for high-performance cross-language service-to-service communication, Connect-RPC for browser-compatible gRPC with a friendlier protocol, and tRPC for TypeScript-only stacks where end-to-end type safety eliminates an entire class of API bugs.

Choosing between them requires understanding what each optimizes for — they're not interchangeable, and the "best" choice depends entirely on your language requirements, performance needs, and team composition.

TL;DR

Use gRPC when you need cross-language service-to-service RPC at high performance — binary Protobuf encoding, HTTP/2 streaming, and generated clients in every language. Use Connect-RPC when you want gRPC's type safety and code generation but need browser compatibility and standard HTTP tooling (curl, fetch). Use tRPC when your entire stack is TypeScript — the end-to-end type inference from database schema to UI is the best developer experience available, with zero code generation.

Key Takeaways

  • gRPC uses Protobuf binary encoding over HTTP/2 — 60-80% smaller payloads than JSON, native streaming, and generated clients for every major language.
  • Connect-RPC supports three protocols: gRPC, gRPC-Web, and the Connect protocol (simple HTTP with JSON or Protobuf). Regular curl and browser fetch work natively.
  • tRPC requires TypeScript on both frontend and backend — the type inference relies on TypeScript's type system; it doesn't work across language boundaries.
  • gRPC requires a .proto schema file and code generation (protoc) — adds build tooling complexity that tRPC eliminates entirely.
  • Connect-RPC is backwards compatible with gRPC — Connect servers handle gRPC clients, and Connect clients can talk to gRPC servers. Migration is gradual.
  • tRPC v11 works with any HTTP framework (Express, Fastify, Next.js, Hono) and integrates natively with TanStack Query on the frontend.
  • Streaming support: gRPC has native bidirectional streaming; tRPC supports subscriptions (WebSocket or SSE); Connect-RPC supports streaming over both gRPC and the Connect protocol.

Protocol Comparison

DimensiongRPCConnect-RPCtRPC
SchemaProtobuf (.proto)Protobuf (.proto)TypeScript types
Wire formatBinary (Protobuf)JSON or BinaryJSON
Browser supportgRPC-Web onlyYes (native HTTP)Yes
Code generationYes (protoc)Yes (buf generate)No
Language supportAll majorAll majorTypeScript only
StreamingYes (bidirectional)YesSubscriptions
Type safetyGenerated typesGenerated typesInferred types
curl-ableNoYesYes

gRPC

Best for: Microservice-to-microservice RPC, cross-language systems, high-performance streaming, polyglot architectures

gRPC is Google's open-source RPC framework — built for high-performance service-to-service communication in polyglot microservice architectures. The Protobuf schema is the source of truth; generated client and server code handles serialization, network communication, and type checking in every supported language (Go, Python, Java, C#, Node.js, Swift, Kotlin, Ruby, PHP).

Service Definition (.proto)

// users.proto
syntax = "proto3";
package users.v1;

service UserService {
  rpc GetUser (GetUserRequest) returns (GetUserResponse);
  rpc ListUsers (ListUsersRequest) returns (stream GetUserResponse);
  rpc CreateUser (CreateUserRequest) returns (GetUserResponse);
  rpc WatchUserEvents (WatchUserEventsRequest) returns (stream UserEvent);
}

message GetUserRequest {
  string id = 1;
}

message GetUserResponse {
  string id = 1;
  string email = 2;
  string name = 3;
  string plan = 4;
  int64 created_at = 5;
}

message UserEvent {
  string user_id = 1;
  string event_type = 2;  // "created" | "updated" | "deleted"
  int64 timestamp = 3;
}

Server Implementation (Go)

// server.go
package main

import (
    "context"
    "net"
    "google.golang.org/grpc"
    pb "your-module/gen/proto/users/v1"
)

type userServer struct {
    pb.UnimplementedUserServiceServer
}

func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
    user, err := db.GetUser(ctx, req.Id)
    if err != nil {
        return nil, status.Errorf(codes.NotFound, "user not found: %s", req.Id)
    }

    return &pb.GetUserResponse{
        Id:    user.ID,
        Email: user.Email,
        Name:  user.Name,
        Plan:  user.Plan,
    }, nil
}

func main() {
    lis, _ := net.Listen("tcp", ":50051")
    s := grpc.NewServer()
    pb.RegisterUserServiceServer(s, &userServer{})
    s.Serve(lis)
}

Client Usage (Node.js)

// client.ts — generated gRPC client
import { UserServiceClient } from "./gen/proto/users/v1/users_grpc_pb";
import { GetUserRequest } from "./gen/proto/users/v1/users_pb";
import * as grpc from "@grpc/grpc-js";

const client = new UserServiceClient(
  "localhost:50051",
  grpc.credentials.createInsecure()
);

// Unary call
const request = new GetUserRequest();
request.setId("user_123");

client.getUser(request, (err, response) => {
  if (err) throw err;
  console.log(`User: ${response.getName()} (${response.getEmail()})`);
});

// Server-side streaming
const listRequest = new ListUsersRequest();
const stream = client.listUsers(listRequest);

stream.on("data", (user) => {
  console.log(`User: ${user.getName()}`);
});

stream.on("end", () => {
  console.log("Stream ended");
});

Performance

gRPC's binary encoding advantage is significant for high-frequency service-to-service calls:

  • JSON: {"id":"user_123","email":"user@example.com","name":"Jane Smith","plan":"pro"} = 79 bytes
  • Protobuf: same data ≈ 35-40 bytes (50%+ smaller)

At 10M requests/day, the bandwidth difference is measurable. For services communicating within a VPC, the performance gain matters more than the serialization overhead.

When to Choose gRPC

Service-to-service communication in polyglot microservices (Go services calling Python services calling Java services), high-frequency APIs where binary encoding's bandwidth and CPU savings are measurable, or architectures requiring native bidirectional streaming (real-time event delivery, monitoring streams).

Connect-RPC

Best for: gRPC compatibility + browser support, teams that want Protobuf type safety with standard HTTP tooling

Connect-RPC (by Buf) implements three protocols simultaneously: the gRPC protocol (for existing gRPC clients), the gRPC-Web protocol (for browser gRPC), and the Connect protocol — a simple HTTP-based protocol that works with curl, browser fetch, and any standard HTTP tooling.

A Connect server is simultaneously a gRPC server, gRPC-Web server, and Connect HTTP server. You write the service once; all three protocols work.

Service Definition (same .proto as gRPC)

// users.proto — identical to gRPC service definition
syntax = "proto3";
package users.v1;

import "google/protobuf/timestamp.proto";

service UserService {
  rpc GetUser (GetUserRequest) returns (GetUserResponse);
  rpc CreateUser (CreateUserRequest) returns (CreateUserResponse);
}

Code Generation with Buf

# buf.gen.yaml — generate Connect TypeScript code
version: v1
plugins:
  - plugin: es
    out: src/gen
  - plugin: connect-es
    out: src/gen
    opt: target=ts

# Generate code
buf generate

Server Implementation (Node.js/Express)

// server.ts — Connect-RPC Node.js server
import { createServer } from "@connectrpc/connect-node";
import { UserService } from "./gen/proto/users/v1/users_connect";
import { GetUserRequest, GetUserResponse } from "./gen/proto/users/v1/users_pb";
import http from "node:http";

const routes = createServer({
  routes(router) {
    router.service(UserService, {
      async getUser(req: GetUserRequest): Promise<GetUserResponse> {
        const user = await db.getUser(req.id);

        return new GetUserResponse({
          id: user.id,
          email: user.email,
          name: user.name,
          plan: user.plan,
        });
      },
    });
  },
});

http.createServer(routes).listen(8080);

Browser Client (No gRPC-Web Proxy Required)

// client.ts — Connect-RPC browser client
// Works in any browser with standard fetch — no proxy needed
import { createPromiseClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { UserService } from "./gen/proto/users/v1/users_connect";

const transport = createConnectTransport({
  baseUrl: "https://api.example.com",
});

const client = createPromiseClient(UserService, transport);

// Type-safe RPC call from the browser
const user = await client.getUser({ id: "user_123" });
console.log(user.email); // TypeScript knows the type

curl-able (Connect Protocol with JSON)

# Connect-RPC endpoints are regular HTTP — curl works
curl -X POST https://api.example.com/users.v1.UserService/GetUser \
  -H "Content-Type: application/json" \
  -d '{"id": "user_123"}'

# Response:
# {"id":"user_123","email":"user@example.com","name":"Jane Smith","plan":"pro"}

When to Choose Connect-RPC

Teams that want gRPC's type safety and Protobuf encoding but need browser compatibility without a gRPC-Web proxy, organizations migrating from REST to Protobuf-based APIs where curl-accessibility during development matters, or existing gRPC users who want to add browser client support without changing their server implementation.

tRPC

Best for: TypeScript monorepos, Next.js full-stack, end-to-end type safety, zero code generation

tRPC is the TypeScript-native RPC framework — no .proto files, no code generation, no build tooling beyond TypeScript itself. You define your API as TypeScript functions; tRPC infers the types on the client automatically. When you change a server function's signature, the TypeScript compiler immediately flags every incorrect client call.

Server Definition

// server/routers/users.ts
import { router, publicProcedure, protectedProcedure } from "../trpc";
import { z } from "zod";

export const usersRouter = router({
  getUser: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input, ctx }) => {
      // input is typed: { id: string }
      const user = await ctx.db.user.findUnique({
        where: { id: input.id },
      });

      if (!user) throw new TRPCError({ code: "NOT_FOUND" });

      return user; // Return type is inferred from Prisma schema
    }),

  createUser: protectedProcedure
    .input(z.object({
      email: z.string().email(),
      name: z.string().min(1),
      plan: z.enum(["free", "pro", "enterprise"]),
    }))
    .mutation(async ({ input, ctx }) => {
      return ctx.db.user.create({ data: input });
    }),

  listUsers: protectedProcedure
    .input(z.object({ limit: z.number().max(100).default(20) }))
    .query(async ({ input, ctx }) => {
      return ctx.db.user.findMany({ take: input.limit });
    }),
});

// Root app router
export const appRouter = router({
  users: usersRouter,
  // auth: authRouter,
  // billing: billingRouter,
});

export type AppRouter = typeof appRouter;

Client Usage (React/Next.js)

// lib/trpc.ts — client setup
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "../server/routers/_app";

export const trpc = createTRPCReact<AppRouter>();
// components/UserProfile.tsx
import { trpc } from "../lib/trpc";

export function UserProfile({ userId }: { userId: string }) {
  // Fully type-safe — TypeScript knows the return type from the server definition
  const { data: user, isLoading } = trpc.users.getUser.useQuery({ id: userId });

  const createUser = trpc.users.createUser.useMutation({
    onSuccess: () => {
      // Automatically invalidate the users list query
      trpc.users.listUsers.invalidate();
    },
  });

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <h1>{user?.name}</h1>       {/* user is typed as User | undefined */}
      <p>{user?.email}</p>
      <p>Plan: {user?.plan}</p>  {/* TypeScript enforces the plan type */}
    </div>
  );
}

Next.js App Router Integration

// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "../../../../server/routers/_app";
import { createTRPCContext } from "../../../../server/trpc";

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
    createContext: createTRPCContext,
  });

export { handler as GET, handler as POST };

When to Choose tRPC

Next.js or TypeScript monorepo projects where frontend and backend are in the same codebase, teams that want to eliminate API contract bugs (wrong field names, wrong types, missing fields) via TypeScript's type system, or applications where zero code generation overhead is valuable.

Limitation: tRPC only works within TypeScript ecosystems. If your mobile app is native Swift/Kotlin, or your backend is Go/Python, you need gRPC or REST — not tRPC.

Decision Matrix

RequirementgRPCConnect-RPCtRPC
Cross-language (Go + Python + Java)YesYesNo
Browser client supportgRPC-Web onlyYes (native)Yes
Zero code generationNoNoYes
TypeScript end-to-end typesGeneratedGeneratedInferred
Protobuf binary encodingYesYes (optional)No
High-performance service meshBestGoodLimited
curl-accessibleNoYesYes
Schema evolution (backward compat)ProtobufProtobufZod schemas
React Query integrationManualManualNative
StreamingBidirectionalYesSubscriptions

Verdict

gRPC is the right choice for high-performance cross-language service-to-service communication — when you have Go services calling Python services calling Java services at high frequency, binary encoding and generated clients across all languages are the correct tools.

Connect-RPC is the right choice when you want gRPC's type safety and Protobuf encoding but need browser support without a proxy, or when you want standard HTTP tooling (curl, logging middleware) to work transparently with your RPC layer.

tRPC is the right choice for TypeScript-only full-stack applications — the end-to-end type inference from database to UI component is the best developer experience available, and the complete elimination of code generation and schema files reduces complexity significantly.


Compare RPC framework documentation, SDK support, and integration guides at APIScout — find the right API communication layer for your architecture.

Comments