Skip to content
tRPC

Partners

Express Adapter

On this page

Example app

Description Links
Express server & procedure calls with Node.js.

How to add tRPC to existing Express project

1. Install deps

yarn add @trpc/server zod

Zod isn't a required dependency, but it's used in the sample router below.

2. Create a tRPC router

Implement your tRPC router. A sample router is given below:

server.ts
import { const initTRPC: TRPCBuilder<object, object>initTRPC } from '@trpc/server';
import { import zz } from 'zod';

export const 
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
= const initTRPC: TRPCBuilder<object, object>initTRPC.
TRPCBuilder<object, object>.create<RuntimeConfigOptions<object, object>>(opts?: RuntimeConfigOptions<object, object> | undefined): TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
create
();
export const
const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    getUser: QueryProcedure<{
        input: string;
        output: {
            id: string;
            name: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
    createUser: MutationProcedure<{
        input: {
            name: string;
        };
        output: {
            name: string;
            id: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
appRouter
=
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.
TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder
<{
    getUser: QueryProcedure<{
        input: string;
        output: {
            id: string;
            name: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
    createUser: MutationProcedure<{
        input: {
            name: string;
        };
        output: {
            name: string;
            id: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>(_: {
    getUser: QueryProcedure<{
        input: string;
        output: {
            id: string;
            name: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
    createUser: MutationProcedure<{
        input: {
            name: string;
        };
        output: {
            name: string;
            id: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}) => BuiltRouter<...>
router
({
getUser: QueryProcedure<{
    input: string;
    output: {
        id: string;
        name: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
}>
getUser
:
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure.ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.input<z.ZodString>(schema: z.ZodString): ProcedureBuilder<object, object, object, string, string, UnsetMarker, UnsetMarker, false, DefaultErrorShape>input(import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string()).
ProcedureBuilder<object, object, object, string, string, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.query<{
    id: string;
    name: string;
}>(resolver: ProcedureResolver<object, object, object, string, UnsetMarker, {
    id: string;
    name: string;
}>): QueryProcedure<{
    input: string;
    output: {
        id: string;
        name: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
}>
query
((opts: ProcedureResolverOptions<object, object, object, string>opts) => {
opts: ProcedureResolverOptions<object, object, object, string>opts.ProcedureResolverOptions<object, object, object, string>.input: stringinput; // string return { id: stringid: opts: ProcedureResolverOptions<object, object, object, string>opts.ProcedureResolverOptions<object, object, object, string>.input: stringinput, name: stringname: 'Bilbo' }; }),
createUser: MutationProcedure<{
    input: {
        name: string;
    };
    output: {
        name: string;
        id: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
}>
createUser
:
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure
.
ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.input<z.ZodObject<{
    name: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    name: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    name: string;
}, {
    name: string;
}, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
input
(import zz.
function object<{
    name: z.ZodString;
}>(shape?: {
    name: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    name: z.ZodString;
}, z.core.$strip>
object
({ name: z.ZodStringname: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string()._ZodString<$ZodStringInternals<string>>.min(minLength: number, params?: string | z.core.$ZodCheckMinLengthParams): z.ZodStringmin(5) }))
.
ProcedureBuilder<object, object, object, { name: string; }, { name: string; }, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.mutation<{
    name: string;
    id: string;
}>(resolver: ProcedureResolver<object, object, object, {
    name: string;
}, UnsetMarker, {
    name: string;
    id: string;
}>): MutationProcedure<{
    input: {
        name: string;
    };
    output: {
        name: string;
        id: string;
    };
    meta: object;
    errorShape: DefaultErrorShape;
}>
mutation
(async (
opts: ProcedureResolverOptions<object, object, object, {
    name: string;
}>
opts
) => {
// use your ORM of choice return { id: stringid: '1', ...
opts: ProcedureResolverOptions<object, object, object, {
    name: string;
}>
opts
.
ProcedureResolverOptions<object, object, object, { name: string; }>.input: {
    name: string;
}
input
};
}), }); // export type definition of API export type
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    getUser: QueryProcedure<{
        input: string;
        output: {
            id: string;
            name: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
    createUser: MutationProcedure<{
        input: {
            name: string;
        };
        output: {
            name: string;
            id: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>> & DecorateCreateRouterOptions<{
    getUser: QueryProcedure<{
        input: string;
        output: {
            id: string;
            name: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
    createUser: MutationProcedure<{
        input: {
            name: string;
        };
        output: {
            name: string;
            id: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>
AppRouter
= typeof
const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    getUser: QueryProcedure<{
        input: string;
        output: {
            id: string;
            name: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
    createUser: MutationProcedure<{
        input: {
            name: string;
        };
        output: {
            name: string;
            id: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
appRouter
;

If your router file starts getting too big, split your router into several subrouters each implemented in its own file. Then merge them into a single root appRouter.

3. Use the Express adapter

tRPC includes an adapter for Express out of the box. This adapter lets you convert your tRPC router into an Express middleware.

server.ts
import { const initTRPC: TRPCBuilder<object, object>initTRPC } from '@trpc/server';
import * as import trpcExpresstrpcExpress from '@trpc/server/adapters/express';
import function express(): Expressexpress from 'express';

// created for each request
const const createContext: ({ req, res, }: trpcExpress.CreateExpressContextOptions) => {}createContext = ({
  req: express.Request<ParamsDictionary, any, any, QueryString.ParsedQs, Record<string, any>>req,
  res: express.Response<any, Record<string, any>>res,
}: import trpcExpresstrpcExpress.
type CreateExpressContextOptions = {
    req: express.Request<ParamsDictionary, any, any, QueryString.ParsedQs, Record<string, any>>;
    res: express.Response<any, Record<string, any>>;
    info: TRPCRequestInfo;
}
CreateExpressContextOptions
) => ({}); // no context
type type Context = {}Context =
type Awaited<T> = T extends null | undefined ? T : T extends object & {
    then(onfulfilled: infer F, ...args: infer _): any;
} ? F extends (value: infer V, ...args: infer _) => any ? Awaited<V> : never : T
Awaited
<type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : anyReturnType<typeof const createContext: ({ req, res, }: trpcExpress.CreateExpressContextOptions) => {}createContext>>;
const
const t: TRPCRootObject<{}, object, RuntimeConfigOptions<{}, object>, {
    ctx: {};
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
= const initTRPC: TRPCBuilder<object, object>initTRPC.TRPCBuilder<object, object>.context<{}>(): TRPCBuilder<{}, object>context<type Context = {}Context>().
TRPCBuilder<{}, object>.create<RuntimeConfigOptions<{}, object>>(opts?: RuntimeConfigOptions<{}, object> | undefined): TRPCRootObject<{}, object, RuntimeConfigOptions<{}, object>, {
    ctx: {};
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
create
();
const
const appRouter: BuiltRouter<{
    ctx: {};
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>
appRouter
=
const t: TRPCRootObject<{}, object, RuntimeConfigOptions<{}, object>, {
    ctx: {};
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.
TRPCRootObject<{}, object, RuntimeConfigOptions<{}, object>, { ctx: {}; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder
<{}>(_: {}) => BuiltRouter<{
    ctx: {};
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>
router
({
// [...] }); const const app: Expressapp = function express(): Expressexpress(); const app: Expressapp.Application<Record<string, any>>.use: <"/trpc", ParamsDictionary, any, any, QueryString.ParsedQs, Record<string, any>>(path: "/trpc", ...handlers: RequestHandler<ParamsDictionary, any, any, QueryString.ParsedQs, Record<string, any>>[]) => Express (+8 overloads)use( '/trpc', import trpcExpresstrpcExpress.
function createExpressMiddleware<BuiltRouter<{
    ctx: {};
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: NodeHTTPHandlerOptions<BuiltRouter<{
    ctx: {};
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>, express.Request<ParamsDictionary, any, any, QueryString.ParsedQs, Record<string, any>>, express.Response<any, Record<string, any>>>): express.Handler
createExpressMiddleware
({
BaseHandlerOptions<BuiltRouter<{ ctx: {}; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>, e.Request<...>>.router: BuiltRouter<{
    ctx: {};
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>
router
:
const appRouter: BuiltRouter<{
    ctx: {};
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>
appRouter
,
createContext?: NodeHTTPCreateContextFn<BuiltRouter<{
    ctx: {};
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>, express.Request<ParamsDictionary, any, any, QueryString.ParsedQs, Record<string, any>>, express.Response<any, Record<string, any>>> | undefined
createContext
,
}), ); const app: Expressapp.Application<Record<string, any>>.listen(port: number, callback?: (error?: Error) => void): Server (+5 overloads)listen(4000);

Your endpoints are now available via HTTP!

EndpointHTTP URI
getUserGET http://localhost:4000/trpc/getUser?input=INPUT

where INPUT is a URI-encoded JSON string.
createUserPOST http://localhost:4000/trpc/createUser

with req.body of type {name: string}

createContext receives Express's req and res, so you can, for example, read req.headers.authorization to authorize the user. The middleware accepts the options every adapter takes, like onError and maxBatchSize, plus middleware and maxBodySize.

Body parsers

tRPC parses request bodies itself, so tRPC routes don't need express.json(). If a body parser runs before tRPC anyway, tRPC uses the req.body it parsed, which works for JSON. For FormData and binary inputs it depends on your Express version:

  • Express 5: express.json() and express.urlencoded() leave other content types unread, so tRPC still reads them.
  • Express 4: the parsers set req.body to {} on content types they skip, so tRPC receives {} instead of the form data or file.
  • In both versions, a parser that reads the body you upload, like express.raw() for application/octet-stream, breaks binary inputs.

To be safe, register body parsers only on the routes that need them:

server.ts
import * as import trpcExpresstrpcExpress from '@trpc/server/adapters/express';
import function express(): Expressexpress from 'express';
import { 
const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>
appRouter
} from './router';
const const app: Expressapp = function express(): Expressexpress(); const app: Expressapp.Application<Record<string, any>>.use: <"/api", {}, any, any, QueryString.ParsedQs, Record<string, any>>(path: "/api", ...handlers: RequestHandler<{}, any, any, QueryString.ParsedQs, Record<string, any>>[]) => Express (+8 overloads)use('/api', function express(): Expressexpress.var e.json: (options?: bodyParser.OptionsJson) => createServer.NextHandleFunctionjson()); // only for your own `/api/*` routes const app: Expressapp.
IRouter.post: IRouterMatcher
<"/api/webhook", {}, any, any, QueryString.ParsedQs, Record<string, any>>(path: "/api/webhook", ...handlers: RequestHandler<{}, any, any, QueryString.ParsedQs, Record<string, any>>[]) => Express (+4 overloads)
post
('/api/webhook', (req: Request<{}, any, any, QueryString.ParsedQs, Record<string, any>>req, res: Response<any, Record<string, any>, number>res) => {
res: Response<any, Record<string, any>, number>res.Response<any, Record<string, any>, number>.json: (body?: any) => Response<any, Record<string, any>, number>json({ received: anyreceived: req: Request<{}, any, any, QueryString.ParsedQs, Record<string, any>>req.Request<{}, any, any, QueryString.ParsedQs, Record<string, any>>.body: anybody }); }); const app: Expressapp.Application<Record<string, any>>.use: <"/trpc", ParamsDictionary, any, any, QueryString.ParsedQs, Record<string, any>>(path: "/trpc", ...handlers: RequestHandler<ParamsDictionary, any, any, QueryString.ParsedQs, Record<string, any>>[]) => Express (+8 overloads)use('/trpc', import trpcExpresstrpcExpress.
function createExpressMiddleware<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: NodeHTTPHandlerOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>, express.Request<ParamsDictionary, any, any, QueryString.ParsedQs, Record<string, any>>, express.Response<any, Record<string, any>>>): express.Handler
createExpressMiddleware
({
BaseHandlerOptions<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>, e.Request<...>>.router: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>
router
:
const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>
appRouter
}));
const app: Expressapp.Application<Record<string, any>>.listen(port: number, callback?: (error?: Error) => void): Server (+5 overloads)listen(4000);