GuidesTypeScript

Fastify with Prisma and React

Create a Fastify API on Node.js with Prisma, PostgreSQL, Better Auth, tRPC, and a React frontend on TanStack Router using Better Fullstack.

Updated October 9, 2026fastifyprismareacttrpc

Use this stack when the API should be a Fastify service on Node.js, with Prisma for PostgreSQL and a React single-page app that calls it through tRPC.

npm create better-fullstack@latest my-fastify-api -- \  --ecosystem typescript \  --frontend tanstack-router \  --backend fastify \  --runtime node \  --database postgres \  --orm prisma \  --auth better-auth \  --api trpc \  --css-framework tailwind \  --ui-library shadcn-ui

What this creates

  • A Fastify server in apps/server with @fastify/cors, a Better Auth route at /api/auth/*, and the tRPC Fastify plugin at /trpc.
  • A React app in apps/web on TanStack Router, with sign-in and sign-up forms, a user menu, a protected dashboard, and a tRPC client on TanStack Query.
  • Prisma 7 on PostgreSQL through the @prisma/adapter-pg driver adapter, with the Better Auth models in their own schema file.
  • Better Auth with email and password sign-in, using the Prisma adapter.
  • Tailwind CSS and shadcn/ui components in the web app.
  • A Turborepo workspace and a reproducible bts.jsonc stack configuration.

Generated shape

txt
my-fastify-api/
  bts.jsonc
  turbo.json
  apps/
    server/
      .env
      src/index.ts
    web/
      .env
      src/
        lib/auth-client.ts
        utils/trpc.ts
        routes/
          index.tsx
          login.tsx
          dashboard.tsx
  packages/
    api/src/
      context.ts
      index.ts
      routers/index.ts
    auth/src/index.ts
    db/
      prisma.config.ts
      prisma/schema/
        schema.prisma
        auth.prisma
      src/index.ts
    env/src/
      server.ts
      web.ts

The Fastify auth route converts each Fastify request into a Fetch Request, passes it to auth.handler, and copies the status, headers, and body back to the reply. The tRPC context converts the Fastify request headers the same way and resolves the Better Auth session from them.

Routes

RoutePurpose
GET /Returns OK.
/api/auth/*Better Auth endpoints for email sign-up, sign-in, session, and sign-out.
/trpc/healthCheckPublic query that returns "OK". The web home page shows its status.
/trpc/privateDataProtected query. It throws UNAUTHORIZED without a session.

Fastify listens on port 3000 with its built-in logger enabled. The web app runs on Vite's port 5173.

Environment

FileVariablePurpose
apps/server/.envDATABASE_URLPostgreSQL connection string used by Prisma and the server.
apps/server/.envBETTER_AUTH_SECRETGenerated random secret, at least 32 characters.
apps/server/.envBETTER_AUTH_URLPublic URL of the server, http://localhost:3000 locally.
apps/server/.envCORS_ORIGINWeb origin the server and Better Auth trust, http://localhost:5173 locally.
apps/web/.envVITE_SERVER_URLServer URL used by the auth client and the tRPC client.

packages/db/prisma.config.ts loads apps/server/.env, so Prisma commands and the server read the same DATABASE_URL.

Run it locally

Point DATABASE_URL at a PostgreSQL database. Installing dependencies runs prisma generate through the database package's postinstall script. Then push the schema and start both apps:

npm installnpm run db:pushnpm run dev

The server's dev script runs tsx watch src/index.ts. Open http://localhost:5173/login to create an account, then visit /dashboard, which calls privateData.

Add a model and a procedure

Add product models to packages/db/prisma/schema/schema.prisma and leave auth.prisma to Better Auth:

prisma
model Project {
  id        String   @id @default(cuid())
  name      String
  ownerId   String
  createdAt DateTime @default(now())

  @@index([ownerId])
}

Apply it and regenerate Prisma Client, which the database package writes to packages/db/prisma/generated:

npm run db:pushnpm run db:generate

Then add a procedure to appRouter in packages/api/src/routers/index.ts. protectedProcedure guarantees ctx.session:

ts
createProject: protectedProcedure
  .input(z.object({ name: z.string().min(2) }))
  .mutation(({ ctx, input }) =>
    prisma.project.create({ data: { name: input.name, ownerId: ctx.session.user.id } }),
  ),

Import prisma as the default export of @my-fastify-api/db and z from zod. The web app calls it with trpc.createProject.mutationOptions().

Compatibility notes

  • --runtime node matches the generated scripts: tsx watch in development and node dist/index.mjs in production.
  • The CLI accepts both --api trpc and --api orpc with Fastify and a React frontend. This guide uses tRPC, which the generated server mounts through the official tRPC Fastify adapter.
  • Prisma uses the prisma-client generator with an ESM output folder, so import the client from the database package rather than from @prisma/client.

Deployment notes

Build with npm run build (tsdown writes apps/server/dist) and start the server with npm run start from apps/server. The server binds to 0.0.0.0:3000. Set DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL, and CORS_ORIGIN in its environment, and build the web app with VITE_SERVER_URL set to the deployed API. For tracked schema changes, create migrations with npm run db:migrate and apply them with prisma migrate deploy before starting new code.

Troubleshooting

SymptomCheck
Imports from prisma/generated failRun npm run db:generate; the client is generated, not committed.
Sign-up fails with a missing table errorRun npm run db:push against the database in DATABASE_URL.
Auth requests return AUTH_FAILURERead the Fastify log; the auth route logs the underlying error before replying with 500.
Browser requests fail CORS checksCORS_ORIGIN must match the web origin exactly, including the port.

Comparison notes

Choose Fastify when the team wants Fastify's plugin system and logging on Node.js. Compare Elysia with Drizzle and Better Auth for a Bun server, or Hono with tRPC and Drizzle for a smaller server that runs on several runtimes.

Next steps

GitHub Sponsors