TypeScript monorepo template

Write the procedure. Get the API, the docs, and the client.

A TypeScript monorepo where one Zod-typed tRPC procedure becomes four things at once: a typed client call, a REST endpoint, an OpenAPI spec, and an interactive playground. No codegen step. No Postman collection to maintain.

$git clone https://github.com/your-org/buildsmoothly.git my-app
  • Next.js 16
  • React 19
  • tRPC 11
  • Drizzle ORM
  • PostgreSQL
  • Turborepo
  • Tailwind v4
  • shadcn/ui

How it works

One procedure. Four artifacts.

This is the whole pitch. Write a procedure with Zod input and output schemas — then look at everything you did not have to write.

You write thisonce
packages/trpc/server/routes/auth/route.ts
import { publicProcedure, router } from "../../trpc";
import { userService } from "../../services";
import { generatePath } from "../../utils/path-generator";
import {
  createUserWithEmailAndPasswordInputModel,
  createUserWithEmailAndPasswordOutputModel,
} from "./model";

const TAGS = ["Authentication"];
const getPath = generatePath("/authentication");

export const authRouter = router({
  createUserWithEmailAndPassword: publicProcedure
    .meta({
      openapi: {
        method: "POST",
        path: getPath("/createUserWithEmailAndPassword"),
        tags: TAGS,
      },
    })
    .input(createUserWithEmailAndPasswordInputModel)
    .output(createUserWithEmailAndPasswordOutputModel)
    .mutation(async ({ input }) => {
      const { fullName, email, password } = input;

      const { id } = await userService.createUserWithEmailAndPassword({
        fullName,
        email,
        password,
      });
      return {
        id,
      };
    }),
});
and you get all of this
You get thesefree, and always in sync

Import the router type once. Every call, argument and response is inferred — rename a field on the server and the frontend stops compiling.

apps/web
import { api } from "~/trpc/server";

const { id } = await api.auth.createUserWithEmailAndPassword.mutate({
  fullName: "Ada Lovelace",
  email: "ada@example.com",
  password: "correct-horse-battery-staple",
});
//      ^? string
//
// Autocomplete knew every field. A misspelled key is a compile error,
// and the response type came from the .output() schema.

You wrote one procedure. You did not write an OpenAPI schema, a fetch wrapper, a response type, or a Postman request.

What you get

Every decision already made, correctly.

Not a pile of dependencies. A set of choices that fit together, each one there because it removes work you would otherwise do by hand.

Docs that cannot go stale

The OpenAPI document is generated from your Zod schemas every boot. There is no YAML file to forget about.

Postman, built in

A Scalar API client lives at /docs. Browse endpoints, send real requests, read responses — without leaving the browser or installing anything.

End-to-end type safety

The web app imports the server's router type directly. Break a contract and TypeScript fails the build, not production.

tRPC and REST from one source

Add openapi meta to a procedure and it is served at /api as REST too. Same handler, same validation, zero duplication.

Environment validated at boot

Every package parses process.env through a Zod schema. A missing DATABASE_URL crashes on line one with a readable message.

A real service layer

Business logic lives in @repo/services, not in your route handlers. Routes stay thin; logic stays testable and reusable.

Drizzle with migrations committed

Typed SQL, generated migrations checked into the repo, and Drizzle Studio wired to pnpm dev.

Structured logging

Winston configured for both worlds: colorized and readable in development, JSON for your log aggregator in production.

Turborepo caching

Task graph and caching configured, remote cache ready. Rebuild only what actually changed.

The UI kit is already installed

Around sixty shadcn/ui components, Tailwind v4, dark mode, toasts and react-hook-form with Zod resolvers. Start on features, not setup.

Postgres in one command

docker compose up brings the database online, and setup.sh links a single root .env into every workspace.

Shared config packages

ESLint and TypeScript configs live in their own workspace packages, so every app inherits identical rules.

Architecture

Layers that stay in their lane.

Apps are thin. Packages hold the substance. Nothing reaches across a boundary it should not, which is why any single piece stays replaceable.

workspace
.
├── apps
│   ├── api            # Express + tRPC + OpenAPI + Scalar docs
│   └── web            # Next.js 16, React 19, shadcn/ui
└── packages
    ├── trpc           # routers, procedures, shared client types
    ├── services       # business logic, transport-agnostic
    ├── database       # Drizzle schema + committed migrations
    ├── logger         # Winston, dev-pretty / prod-JSON
    ├── eslint-config
    └── typescript-config

The type seam

  1. apps/web
  2. @repo/trpc/client
  3. @repo/trpc/server
  4. apps/api

The web app never imports server code — only its type. Contracts are shared at compile time and erased at runtime.

The request path

  1. @repo/trpc/server
  2. @repo/services
  3. @repo/database

Routes validate and delegate. Services own the logic. The database package owns the schema. Each layer is replaceable.

The daily loop

Add an endpoint in about a minute.

Four steps, start to finish. This is the real auth route from the template, not a simplified illustration.

1

Describe the shapes

Those .describe() calls are your API documentation. Write them once, here.

packages/trpc/server/routes/auth/model.ts
import { z } from "zod";

export const createUserWithEmailAndPasswordInputModel = z.object({
  fullName: z.string().describe("name of the user"),
  email: z.email().describe("email of the user"),
  password: z.string().describe("password of the user"),
});

export const createUserWithEmailAndPasswordOutputModel = z.object({
  id: z.string().describe("id of the user created"),
});
2

Write the procedure

generatePath keeps REST paths consistent. The tags group your endpoints in the docs sidebar.

packages/trpc/server/routes/auth/route.ts
const TAGS = ["Authentication"];
const getPath = generatePath("/authentication");

export const authRouter = router({
  createUserWithEmailAndPassword: publicProcedure
    .meta({
      openapi: {
        method: "POST",
        path: getPath("/createUserWithEmailAndPassword"),
        tags: TAGS,
      },
    })
    .input(createUserWithEmailAndPasswordInputModel)
    .output(createUserWithEmailAndPasswordOutputModel)
    .mutation(async ({ input }) => {
      const { fullName, email, password } = input;

      const { id } = await userService.createUserWithEmailAndPassword({
        fullName,
        email,
        password,
      });
      return { id };
    }),
});
3

Mount it

One line. This is the only registration step there is.

packages/trpc/server/index.ts
export const serverRouter = router({
  auth: authRouter,
});

export type ServerRouter = typeof serverRouter;
4

Call it, fully typed

No client to regenerate, no SDK to publish. The type flowed straight through.

apps/web/app/signup/page.tsx
const { id } = await api.auth.createUserWithEmailAndPassword.mutate({
  fullName: "Ada Lovelace",
  email: "ada@example.com",
  password: "correct-horse-battery-staple",
});
//      ^? string

// Autocomplete knew every field. Typos are compile errors.

Meanwhile the REST route, the OpenAPI spec and the /docs playground updated themselves. You never opened a second tool.

The difference

What you would have built anyway.

Nothing here is impossible to assemble yourself. The question is whether you want to spend the first week of the project doing it.

ConcernStarting from scratchbuildsmoothly
API documentationHand-written Swagger YAML that drifts from the codeGenerated from the Zod schemas on every boot
Trying an endpointA Postman collection someone has to export and shareInteractive client at /docs, always matching the code
Frontend typesInterfaces retyped by hand, or a codegen step in CIInferred from the router type — no generation step
REST and RPCTwo handlers, two validators, two chances to disagreeOne procedure, served on both transports
Environment variablesprocess.env.FOO! and a crash at 3amZod-parsed at startup in every package
Database migrationsBolted on once the schema already hurtsDrizzle configured, migrations committed from day one
UI componentsA week of wiring Tailwind, Radix and dark mode~60 shadcn/ui components installed and themed
Build timesRebuild everything, every timeTurborepo task graph with caching

Quickstart

Five commands to a running stack.

Database, API, docs, playground and web app — all up, all talking to each other.

  1. 01Start Postgres
    $docker compose up -d
  2. 02Install dependencies
    $pnpm install
  3. 03Link the shared .env
    $./setup.sh
  4. 04Run migrations
    $pnpm db:migrate
  5. 05Start everything
    $pnpm dev

Then everything is here

  • Web apphttp://localhost:3000Next.js
  • APIhttp://localhost:8000Express + tRPC
  • API playgroundhttp://localhost:8000/docsScalar client
  • OpenAPI spechttp://localhost:8000/openapi.jsongenerated
  • tRPC endpointhttp://localhost:8000/trpctyped transport
  • Postgreslocalhost:5433docker compose

Open the playground and send a signup request. That round trip — validated input, a row in Postgres, a typed response — is the whole template working end to end.

Pricing

One price. Everything in it.

No seats, no tiers, no subscription. Pay once and the whole repository is yours to build on.

99

one-time · lifetime access

Less than the hour you would spend wiring OpenAPI by hand.

Buy and clone
  • The complete monorepo source — nothing obfuscated, nothing held back
  • Typed tRPC layer with OpenAPI docs generated from your schemas
  • Built-in API playground at /docs, so no Postman collection to maintain
  • Drizzle schema, committed migrations and a Dockerised Postgres
  • ~60 shadcn/ui components, themed, with dark mode already wired
  • Zod-validated environment config across every package
  • Free updates — pull them whenever you want them

What the licence allows

  • Use it on unlimited personal and client projects
  • Modify anything, ship it closed-source, no attribution needed
  • Resell or redistribute the template itself

Questions

Before you ask.

Skip the week of plumbing.

Auth scaffolding, typed API layer, generated docs, an API client, migrations and a themed component library — configured, wired together, and working the moment you clone it.

$git clone https://github.com/your-org/buildsmoothly.git my-app

₹99 once. Full source, unlimited projects, free updates.

buildsmoothlyBuilt with Next.js, tRPC, Drizzle and Turborepo.