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.
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,
};
}),
});Import the router type once. Every call, argument and response is inferred — rename a field on the server and the frontend stops compiling.
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.
.
├── 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-configThe type seam
apps/web@repo/trpc/client@repo/trpc/serverapps/api
The web app never imports server code — only its type. Contracts are shared at compile time and erased at runtime.
The request path
@repo/trpc/server@repo/services@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.
Describe the shapes
Those .describe() calls are your API documentation. Write them once, here.
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"),
});Write the procedure
generatePath keeps REST paths consistent. The tags group your endpoints in the docs sidebar.
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 };
}),
});Mount it
One line. This is the only registration step there is.
export const serverRouter = router({
auth: authRouter,
});
export type ServerRouter = typeof serverRouter;Call it, fully typed
No client to regenerate, no SDK to publish. The type flowed straight through.
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.
| Concern | Starting from scratch | buildsmoothly |
|---|---|---|
| API documentation | Hand-written Swagger YAML that drifts from the code | Generated from the Zod schemas on every boot |
| Trying an endpoint | A Postman collection someone has to export and share | Interactive client at /docs, always matching the code |
| Frontend types | Interfaces retyped by hand, or a codegen step in CI | Inferred from the router type — no generation step |
| REST and RPC | Two handlers, two validators, two chances to disagree | One procedure, served on both transports |
| Environment variables | process.env.FOO! and a crash at 3am | Zod-parsed at startup in every package |
| Database migrations | Bolted on once the schema already hurts | Drizzle configured, migrations committed from day one |
| UI components | A week of wiring Tailwind, Radix and dark mode | ~60 shadcn/ui components installed and themed |
| Build times | Rebuild everything, every time | Turborepo 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.
- 01Start Postgres$
docker compose up -d - 02Install dependencies$
pnpm install - 03Link the shared .env$
./setup.sh - 04Run migrations$
pnpm db:migrate - 05Start everything$
pnpm dev
Then everything is here
- Web app
http://localhost:3000Next.js - API
http://localhost:8000Express + tRPC - API playground
http://localhost:8000/docsScalar client - OpenAPI spec
http://localhost:8000/openapi.jsongenerated - tRPC endpoint
http://localhost:8000/trpctyped transport - Postgres
localhost: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.
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.