This episode completes the production cycle: a CI pipeline that enforces type-safe contracts and linting, unit and integration testing with vitest, msw, and procedure snapshots, as well as production deployment with environment configuration, observability, and release flow.

Code that only passes on a local machine isn't enough. Episode 17 takes you through the full production cycle: a CI pipeline that enforces type-safe contracts and linting, unit and integration testing, as well as deployment with environment configuration, observability, and release flow.
The advantage of tRPC shows again: because the contract is typed, the CI build pipeline can catch violations that in REST would only be felt in production.
A CI pipeline for a tRPC project at least runs: install dependencies, typecheck, lint, and test. An example GitHub Actions workflow:
name: CI
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx tsc --noEmit
- run: npm run lint
- run: npm testnpx tsc --noEmit checks the entire project without producing files — this is where tRPC contract violations are detected: if the client uses a procedure that doesn't exist or input with the wrong shape, compilation fails before deployment.
Because the client and server share types, CI automatically enforces contract consistency. This is far stricter than a REST pipeline that only checks that code compiles separately. Every pull request that changes a router immediately validates all of its usages.
Unit testing tRPC procedures uses the caller from episode 2 — without HTTP, yet still running middleware and validation:
import { describe, it, expect } from "vitest";
import { createCaller } from "../server/root";
const caller = createCaller({ userId: 1 });
describe("userRouter", () => {
it("mengembalikan user sesuai id", async () => {
const user = await caller.user.byId({ id: 1 });
expect(user.nama).toBe("Arman");
});
it("menolak input yang tidak valid", async () => {
await expect(caller.user.byId({ id: "abc" as never }))
.rejects.toThrow();
});
});createCaller({ userId: 1 }) creates a caller with a test context. Testing validation is also easy: sending a wrongly-shaped input throws a zod error.
Snapshot testing captures the output shape of a procedure. When the output changes, the test flags the change so you're aware of its effects:
it("output user.byId sesuai snapshot", async () => {
const user = await caller.user.byId({ id: 1 });
expect(user).toMatchInlineSnapshot();
});toMatchInlineSnapshot stores the exact output shape on the first call. Subsequent output changes become visible in the review diff — very helpful for maintaining the backward compatibility from episode 9.
For integration testing from the client side, Mock Service Worker (msw) intercepts network requests and returns predefined responses:
npm install --save-dev vitest mswimport { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
const server = setupServer(
http.post("/api/trpc/user.byId", () =>
HttpResponse.json({
result: { data: { id: 1, nama: "Arman" } },
}),
),
);
beforeAll(() => server.listen());
afterAll(() => server.close());setupServer intercepts POST requests to the tRPC endpoint and returns mock data. With this, React components can be tested without a real server, while still exercising the tRPC transport mechanism.
Production deployment uses the environment variables arranged from episode 7:
NODE_ENV=production
NEXT_PUBLIC_BASE_URL=https://api.contoh.com
DATABASE_URL=postgres://user:password@host:5432/db
REDIS_URL=redis://cache.internal:6379Note that secrets such as DATABASE_URL and REDIS_URL must not live in the repository — put them in your platform's secret store and inject them at deploy time.
Production needs the observability from episode 14: structured logs, metrics, and tracing active from day one. For the release flow, apply versioning with tags:
git tag v1.4.0
git push origin v1.4.0Incremental releases with semver give you a safety net: if a new version misbehaves, roll back to the previous tag. Combine this with the deprecation policy from episode 9 so client migrations always stay under control.
Info
Set up continuous integration on PRs to run tsc --noEmit and tests first. A build that passes slowly in the pipeline means tRPC contract regressions are caught during review, not after merge.
Episode 17 completes the production cycle: a CI pipeline enforces type-safe contracts and linting, unit and integration tests keep procedure behavior in check, and production deployment runs with environment configuration, observability, and a well-ordered release flow.
Key takeaways:
tsc --noEmit, lint, and test.In the next episode, episode 18 — the final episode of the series — we will discuss modern tooling & the latest stable features — @trpc/next, @trpc/react-query, @trpc/server, and createTRPCProxyClient, stable features like router.merge, procedure.input/output, and links, as well as the full-stack type safety, monorepo, and API-first DX trends with tRPC.