| name | typescript-full-stack |
| description | Use when building end-to-end TypeScript applications — Node backend (Fastify), React/Next frontend, shared types via tRPC or Zod, monorepo with turborepo/nx, Prisma/Drizzle data layer, end-to-end type safety. |
| metadata | {"portable":true,"compatible_with":["Codex","codex"]} |
Full-Stack TypeScript
Acknowledgement: Shared by Peter Bamuhigire, techguypeter.com, +256 784 464178.
Use When
- Use when building end-to-end TypeScript applications — Node backend (Fastify), React/Next frontend, shared types via tRPC or Zod, monorepo with turborepo/nx, Prisma/Drizzle data layer, end-to-end type safety.
Evidence Produced
| Category | Artifact | Format | Example |
|---|
| Correctness | Full-stack contract test plan | Markdown doc covering Fastify route + tRPC router + Prisma schema contract tests | docs/ts/full-stack-tests.md |
References
- Use the
references/ directory for deep detail after reading the core workflow below.
Build and maintain apps where the same types flow from database to backend to frontend to mobile. Zero runtime type drift between layers.
Prerequisites: Load typescript-effective for production idioms. Load typescript-mastery for type-system depth.
When this skill applies
- Greenfield TypeScript web app (React + Node).
- Unifying a split JS frontend + JS backend under TypeScript.
- Refactoring an Express/JS backend into a typed Fastify/TS backend while keeping the frontend running.
- Setting up a monorepo with shared types across packages.
Architecture baseline
+----------------------------------+
| apps/web (Next.js) |
| apps/mobile (Expo RN) |
| apps/api (Fastify) |
| packages/schemas (Zod) |---> source of truth for types
| packages/api-client (tRPC / fetch)
| packages/db (Prisma client)
| packages/ui (shadcn-like components)
| packages/config (tsconfig, eslint, prettier)
+----------------------------------+
- Zod schemas are the single source of truth; TS types are inferred.
- tRPC for internal APIs (no OpenAPI needed — types flow).
- REST + OpenAPI only when external clients consume the API. Use
ts-rest or zod-to-openapi.
- Prisma for SQL-first data layer; Drizzle when you want SQL in view and finer control.
- Auth via Lucia, better-auth, or Clerk — not rolling your own.
Monorepo with turborepo
pnpm init -w
pnpm add -D turbo
turbo.json defines pipeline (build, test, lint, type-check) with caching. Remote cache on Vercel or self-hosted.
Rule: every package has its own tsconfig.json extending packages/config/tsconfig.base.json. Project references enable incremental typecheck.
See references/monorepo-turborepo.md.
Fastify backend (TS-first)
import Fastify from "fastify";
import { z } from "zod";
const app = Fastify({ logger: true });
const Params = z.object({ id: z.string().uuid() });
app.get("/users/:id", async (req, reply) => {
const { id } = Params.parse(req.params);
const user = await db.user.findUnique({ where: { id } });
if (!user) return reply.code(404).send({ error: "not_found" });
return user;
});
Fastify beats Express for TS: first-class hooks, schema validation, plugin system, lifecycle typing. See references/fastify-backend.md.
tRPC — end-to-end types without OpenAPI
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();
export const appRouter = t.router({
user: t.router({
byId: t.procedure
.input(z.object({ id: z.string().uuid() }))
.query(async ({ input }) => db.user.findUnique({ where: { id: input.id } })),
create: t.procedure
.input(UserCreate)
.mutation(async ({ input }) => db.user.create({ data: input })),
}),
});
export type AppRouter = typeof appRouter;
Frontend imports AppRouter type only. Runtime is fetch; types flow at compile time. Zero duplication. See references/trpc-end-to-end.md.
When tRPC vs REST:
All clients are your own code, no public API -> tRPC
External partners, third-party integrations, public SDK -> REST + OpenAPI
Mobile on old TS versions / no TS -> REST
GraphQL ecosystem in the org -> GraphQL (not covered here)
REST + OpenAPI (when needed)
Use ts-rest or zod-to-openapi — define schemas once, generate OpenAPI spec automatically.
import { initContract } from "@ts-rest/core";
const c = initContract();
export const userContract = c.router({
getUser: {
method: "GET",
path: "/users/:id",
pathParams: z.object({ id: z.string().uuid() }),
responses: { 200: UserSchema, 404: z.object({ error: z.literal("not_found") }) },
},
});
See references/rest-plus-openapi.md.
Data layer — Prisma or Drizzle
Prisma: schema-first, Prisma Client is fully typed, great DX. Ships with migration tool. Best for teams.
Drizzle: SQL-first, TypeScript-first. Queries look like SQL, no hidden abstractions. Best when you love SQL and want explicit control.
Decision rule:
Want schema.prisma as source of truth, good migrations, team of mixed SQL skill -> Prisma
Want SQL-in-code visibility, complex queries, performance-critical -> Drizzle
Postgres-only with advanced features (arrays, ranges, custom types) -> Drizzle
MySQL primary, migrations managed alongside PHP app -> Prisma
See references/prisma-vs-drizzle.md.
Zod schemas as the shared layer
import { z } from "zod";
export const UserCreate = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(["admin", "member", "viewer"]),
}).strict();
export const User = UserCreate.extend({
id: z.string().uuid(),
createdAt: z.coerce.date(),
});
export type UserCreate = z.infer<typeof UserCreate>;
export type User = z.infer<typeof User>;
Consumed by: API validation, Prisma custom types, React Hook Form, tRPC input, test fixtures. See references/zod-shared-schemas.md.
Auth
- Lucia — session-based, database-backed, flexible. Good for first-party apps.
- better-auth — newer, feature-rich (MFA, OAuth, magic links).
- Clerk — hosted auth; trade cost for speed. Great for MVPs.
- NextAuth/Auth.js — incumbent; fine but DX can be rough.
Never roll password hashing, session storage, or OAuth flows yourself. See references/auth-patterns.md.
Testing the full stack
- Unit: vitest in each package.
- Integration: vitest + test database (Testcontainers or pg-lite).
- E2E: Playwright for the web app.
- Contract tests: if you expose REST, snapshot the OpenAPI spec — breaking changes fail CI.
- Type tests:
expectTypeOf for API client usage.
See references/testing-full-stack.md.
Deployment — Docker for Node
Multi-stage Dockerfile, node-slim base, non-root user, pnpm deploy for prod-only deps:
FROM node:22-slim AS base
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
FROM base AS deps
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY apps/api/package.json ./apps/api/
COPY packages/ ./packages/
RUN pnpm install --frozen-lockfile
FROM deps AS build
COPY . .
RUN pnpm --filter api build
FROM base AS prod
ENV NODE_ENV=production
USER node
WORKDIR /app
COPY --from=build --chown=node:node /app/apps/api/dist ./dist
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]
See references/docker-node-production.md.
Anti-patterns
- Duplicating types between frontend and backend ("I'll just type it again here").
- Trusting API responses without Zod validation on the client.
- Running migrations in app startup — run in CI or a separate migrate step.
- Sharing Prisma client across edge + Node runtime without a driver adapter.
- Importing server code into client code (mark server-only with
"use server" or a runtime guard).
- Monorepo without remote build cache on CI — slow feedback.
any on API boundaries.
Read next
typescript-effective — idioms.
typescript-mastery — type system depth.
react-development / nextjs-app-router — frontend.
nodejs-development — Node runtime patterns.
postgresql-fundamentals / mysql-best-practices — data layer specifics.
References
references/monorepo-turborepo.md
references/fastify-backend.md
references/trpc-end-to-end.md
references/rest-plus-openapi.md
references/prisma-vs-drizzle.md
references/zod-shared-schemas.md
references/auth-patterns.md
references/testing-full-stack.md
references/docker-node-production.md
references/large-scale-react-ts.md
Decision Rules
| Condition | Action |
|---|
| Only first-party TypeScript clients consume API | Consider tRPC with runtime validation |
| External clients or long-lived integrations exist | Publish REST/OpenAPI contracts |
| Shared package creates circular dependencies | Split schemas from implementations |
Capability Contract
Read and search are required. Editing, database mutation, builds, and tests require authorisation.
Degraded Mode
Fallback: without execution or services, return unverified integration points and exact end-to-end checks. Validate every network boundary at runtime.
Inputs
| Artefact | Required? | Purpose |
|---|
| Frontend/backend boundaries, shared contracts, runtime targets, and deployment context | yes | Coordinate full-stack types and behaviour |
Outputs
- Produce end-to-end TypeScript implementation or design with shared contracts, tests, and deployment notes.