0Pricing
TypeScript Academy · Lesson

Defining Routers and Procedures

Build queries and mutations with input validation.

Defining Routers and Procedures is a free TypeScript Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the TypeScript Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Initializing tRPC

Every tRPC backend starts by creating an instance with initTRPC. It returns builders for routers and procedures. You usually create it once and export the pieces you need.

import { initTRPC } from '@trpc/server';

const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;

A First Procedure

A procedure is a single API endpoint. The simplest one is a query that returns a value. publicProcedure.query defines a readable endpoint with no input.

import { router, publicProcedure } from './trpc';

const appRouter = router({
  health: publicProcedure.query(() => 'ok'),
});

Validating Input

Procedures declare their input with a validator, commonly a zod schema. .input(schema) both validates at runtime and infers the input type for the resolver and the client.

import { z } from 'zod';

const greet = publicProcedure
  .input(z.object({ name: z.string() }))
  .query(({ input }) => 'Hello, ' + input.name);

Reading the Input

Inside the resolver, the validated input is available as input and is fully typed from the schema. There is no manual casting; zod and tRPC infer the shape together.

const byId = publicProcedure
  .input(z.object({ id: z.number() }))
  .query(({ input }) => {
    // input.id is number
    return { id: input.id, name: 'Alice' };
  });

Mutations

Use .mutation for operations that change data. The shape is the same as a query but signals a write, which affects client caching and HTTP method.

const createUser = publicProcedure
  .input(z.object({ name: z.string() }))
  .mutation(({ input }) => {
    return { id: 1, name: input.name };
  });

Building the App Router

Collect procedures into a router by passing an object to router(...). Each key becomes a callable procedure name. This appRouter is your whole API.

const appRouter = router({
  greet,
  byId,
  createUser,
});

Nested Routers

Routers nest to organize large APIs. A value in the router object can itself be a router, producing namespaced calls like user.byId.

const userRouter = router({ byId, createUser });

const appRouter = router({
  user: userRouter,
  greet,
});
// call paths: user.byId, user.createUser, greet

Exporting the Router Type

The single most important export is the router type. The client will import it to gain full type safety. Export the value for the server adapter and the type for the client.

export const appRouter = router({ user: userRouter });
export type AppRouter = typeof appRouter;

Attaching an Adapter

To serve the router over HTTP, attach an adapter. tRPC provides adapters for standalone Node, Express, Next.js, Fetch, and more. The adapter turns incoming requests into procedure calls.

import { createHTTPServer } from '@trpc/server/adapters/standalone';

createHTTPServer({ router: appRouter }).listen(3000);

Organizing the Files

A common layout: a trpc.ts with initTRPC and the exported router/publicProcedure, feature routers in their own files, and a root appRouter that composes them. Keeping initTRPC in one place avoids multiple instances.

// trpc.ts: initTRPC.create(), export router + publicProcedure
// routers/user.ts: userRouter
// router.ts: appRouter = router({ user: userRouter })

Input Inference Recap

Because .input(schema) both validates and infers, you write the schema once and get a runtime guard plus a static type. The client will see exactly the input each procedure expects, derived from these schemas.

const search = publicProcedure
  .input(z.object({ q: z.string(), page: z.number().optional() }))
  .query(({ input }) => ({ q: input.q, page: input.page ?? 1 }));

Quick Check

Test your understanding of routers and procedures.

Recap

You built a tRPC backend.

  • initTRPC.create() yields router and procedure builders.
  • .input(schema) validates and infers input.
  • .query reads; .mutation writes.
  • Compose procedures into appRouter and export type AppRouter = typeof appRouter.

Next: consuming it with a fully typed client.

Frequently asked questions

Is the “Defining Routers and Procedures” lesson free?

Yes — the full text of “Defining Routers and Procedures” is free to read here on the web, and the TypeScript Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the TypeScript Academy course, upgrade to CoddyKit PRO.

What will I learn in “Defining Routers and Procedures”?

Build queries and mutations with input validation. You practise TypeScript Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start TypeScript Academy?

No prior experience is required. TypeScript Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Defining Routers and Procedures” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this TypeScript Academy lesson?

Yes. Every TypeScript Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. The tRPC Architecture
  2. Defining Routers and Procedures
  3. Client-Server Type Inference
  4. Middleware and Context
← Back to TypeScript Academy