backend-master

Unified decision framework for TypeScript backend development.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "backend-master" with this command: npx skills add petbrains/mvp-builder/petbrains-mvp-builder-backend-master

Backend Master Skill

Unified decision framework for TypeScript backend development.

Stack: Node.js · TypeScript · tRPC/Express · Prisma · Zod · Vitest · Docker

Quick Decision Matrix

WHAT DO YOU NEED? │ ├─► API Layer │ ├─ Full-stack TypeScript app → tRPC [skill: backend-trpc] │ ├─ Need REST for external clients → tRPC + OpenAPI [skill: backend-trpc-openapi] │ └─ Pure Express API → Express + Zod │ ├─► Authentication │ ├─ Next.js App Router → Auth.js [skill: backend-auth-js] │ └─ Express/pure API → Passport.js [skill: backend-passport-js] │ ├─► Database │ └─ TypeScript + SQL → Prisma [skill: backend-prisma] │ ├─► Validation │ └─ Any input validation → Zod [skill: backend-zod] │ ├─► Observability │ └─ Structured logging → Pino [skill: backend-pino] │ ├─► Testing │ └─ Unit/integration tests → Vitest [skill: backend-vitest] │ └─► Deployment └─ Containerization → Docker [skill: docker-node]

  1. Project Setup Checklist

New tRPC + Prisma Project

Initialize

mkdir my-api && cd my-api npm init -y

Core dependencies

npm install @trpc/server zod @prisma/client pino npm install -D typescript @types/node prisma vitest

Initialize TypeScript

npx tsc --init

Initialize Prisma

npx prisma init

Recommended tsconfig.json

{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "outDir": "dist", "rootDir": "src", "declaration": true, "resolveJsonModule": true, "baseUrl": ".", "paths": { "@/": ["src/"] } }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] }

Recommended Structure

src/ ├── server/ │ ├── trpc.ts # tRPC instance, base procedures │ ├── context.ts # Request context │ └── routers/ │ ├── _app.ts # Root router (merges all) │ ├── user.ts # User procedures │ └── post.ts # Post procedures ├── lib/ │ ├── prisma.ts # Prisma singleton │ ├── logger.ts # Pino configuration │ └── env.ts # Environment validation ├── schemas/ │ ├── user.schema.ts # User Zod schemas │ └── common.schema.ts # Shared schemas ├── middleware/ │ ├── auth.ts # Auth middleware │ └── logging.ts # Request logging └── index.ts # Entry point

prisma/ ├── schema.prisma # Database schema └── migrations/ # Migration history

test/ ├── setup.ts # Test setup └── context.ts # Mock context factory

  1. API Layer Decision

tRPC vs REST Decision Tree

Building an API? │ ├─► Who are the clients? │ │ │ ├─► Only TypeScript (Next.js, React) │ │ └─► Pure tRPC ✓ │ │ - End-to-end type safety │ │ - No code generation │ │ - Automatic request batching │ │ │ ├─► TypeScript + external clients (mobile, third-party) │ │ └─► tRPC + OpenAPI ✓ │ │ - Type-safe internal API │ │ - REST endpoints for external │ │ - Swagger documentation │ │ │ └─► Only external/non-TypeScript clients │ └─► Express + OpenAPI ✓ │ - Standard REST │ - Maximum compatibility

tRPC Quick Setup

→ See [backend-trpc] for full guide

// src/server/trpc.ts import { initTRPC, TRPCError } from '@trpc/server'; import { z } from 'zod';

interface Context { user?: { id: string; role: string }; db: PrismaClient; log: Logger; }

const t = initTRPC.context<Context>().create();

export const router = t.router; export const publicProcedure = t.procedure; export const middleware = t.middleware;

// Auth middleware const isAuthed = middleware(async ({ ctx, next }) => { if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' }); return next({ ctx: { user: ctx.user } }); });

export const protectedProcedure = publicProcedure.use(isAuthed);

When to Add OpenAPI

→ See [backend-trpc-openapi] for full guide

// Add OpenAPI meta to expose as REST .meta({ openapi: { method: 'GET', path: '/users/{id}', tags: ['Users'], }, })

Scenario Recommendation

Internal TypeScript clients Pure tRPC

Third-party integrations tRPC + OpenAPI

Public API documentation tRPC + OpenAPI

Mobile apps (non-React Native) tRPC + OpenAPI

Microservices (mixed languages) OpenAPI/REST

  1. Authentication Decision

Auth.js vs Passport.js

Need authentication? │ ├─► Next.js App Router? │ └─► Auth.js (NextAuth.js v5) ✓ │ - Native Next.js integration │ - OAuth providers built-in │ - Serverless/Edge ready │ └─► Express.js / Pure API? └─► Passport.js ✓ - JWT authentication - 500+ strategies - Maximum control

Auth.js Quick Setup (Next.js)

→ See [backend-auth-js] for full guide

// auth.ts import NextAuth from 'next-auth'; import GitHub from 'next-auth/providers/github'; import { PrismaAdapter } from '@auth/prisma-adapter';

export const { handlers, auth, signIn, signOut } = NextAuth({ adapter: PrismaAdapter(prisma), session: { strategy: 'jwt' }, providers: [GitHub], callbacks: { jwt({ token, user }) { if (user) token.id = user.id; return token; }, session({ session, token }) { session.user.id = token.id as string; return session; }, }, });

Passport.js Quick Setup (Express)

→ See [backend-passport-js] for full guide

// src/strategies/jwt.strategy.ts import passport from 'passport'; import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';

passport.use(new JwtStrategy({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: process.env.JWT_SECRET!, }, async (payload, done) => { const user = await prisma.user.findUnique({ where: { id: payload.sub } }); return done(null, user || false); }));

Feature Auth.js Passport.js

Best for Next.js Express

OAuth setup Minimal Manual

JWT support Built-in passport-jwt

Session storage JWT/DB Manual

Serverless Yes Limited

Strategies ~20 500+

  1. Database Layer (Prisma)

→ See [backend-prisma] for full guide

Singleton Pattern (Required)

// src/lib/prisma.ts import { PrismaClient } from '@prisma/client';

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

export const prisma = globalForPrisma.prisma || new PrismaClient({ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], });

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

Essential Schema Patterns

// prisma/schema.prisma model User { id String @id @default(cuid()) email String @unique name String? role Role @default(USER) posts Post[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt

@@index([email]) }

model Post { id String @id @default(cuid()) title String @db.VarChar(255) published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId String

@@index([authorId]) @@index([createdAt(sort: Desc)]) }

enum Role { USER ADMIN }

Migration Commands

npx prisma migrate dev --name init # Development npx prisma migrate deploy # Production npx prisma generate # Regenerate client npx prisma studio # GUI viewer

  1. Validation Layer (Zod)

→ See [backend-zod] for full guide

Core Patterns

// src/schemas/user.schema.ts import { z } from 'zod';

// Base schema export const UserSchema = z.object({ id: z.string().cuid(), email: z.string().email(), name: z.string().min(2).max(100), role: z.enum(['USER', 'ADMIN']), });

// Derive variations export const CreateUserSchema = UserSchema.omit({ id: true }); export const UpdateUserSchema = CreateUserSchema.partial();

// Infer types export type User = z.infer<typeof UserSchema>; export type CreateUser = z.infer<typeof CreateUserSchema>;

Common Schemas

// src/schemas/common.schema.ts export const PaginationSchema = z.object({ limit: z.number().min(1).max(100).default(10), cursor: z.string().optional(), });

export const IdSchema = z.object({ id: z.string().cuid(), });

// Environment validation export const EnvSchema = z.object({ NODE_ENV: z.enum(['development', 'production', 'test']), DATABASE_URL: z.string().url(), JWT_SECRET: z.string().min(32), PORT: z.coerce.number().default(3000), });

export const env = EnvSchema.parse(process.env);

Zod + tRPC Integration

// Zod validates input automatically export const userRouter = router({ create: protectedProcedure .input(CreateUserSchema) .mutation(({ input, ctx }) => { // input is typed as CreateUser return ctx.db.user.create({ data: input }); }), });

  1. Logging (Pino)

→ See [backend-pino] for full guide

Configuration

// src/lib/logger.ts import pino from 'pino';

const isDev = process.env.NODE_ENV === 'development';

export const logger = pino({ level: process.env.LOG_LEVEL || (isDev ? 'debug' : 'info'),

transport: isDev ? { target: 'pino-pretty', options: { colorize: true }, } : undefined,

redact: { paths: ['password', 'token', '*.password', 'req.headers.authorization'], censor: '[REDACTED]', },

base: { service: process.env.SERVICE_NAME || 'api', env: process.env.NODE_ENV, }, });

Request Logging Middleware

// src/middleware/logging.ts export function requestLogger(req: Request, res: Response, next: NextFunction) { const requestId = req.headers['x-request-id'] || randomUUID(); const start = Date.now();

req.log = logger.child({ requestId, method: req.method, path: req.path }); req.log.info('Request started');

res.on('finish', () => { req.log.info({ statusCode: res.statusCode, duration: Date.now() - start }, 'Request completed'); });

next(); }

Structured Logging Rules

// ❌ String interpolation logger.info(User ${userId} logged in from ${ip});

// ✅ Structured objects logger.info({ userId, ip, action: 'login' }, 'User logged in');

  1. Testing (Vitest)

→ See [backend-vitest] for full guide

Configuration

// vitest.config.ts import { defineConfig } from 'vitest/config'; import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({ plugins: [tsconfigPaths()], test: { globals: true, environment: 'node', include: ['/*.test.ts'], setupFiles: ['./test/setup.ts'], coverage: { provider: 'v8', include: ['src//*.ts'], }, }, });

Mock Context Factory

// test/context.ts import { mockDeep, DeepMockProxy } from 'vitest-mock-extended'; import { PrismaClient } from '@prisma/client';

export type MockContext = { prisma: DeepMockProxy<PrismaClient>; user: { id: string; role: string } | null; };

export const createMockContext = (user = null): MockContext => ({ prisma: mockDeep<PrismaClient>(), user, });

Testing tRPC Procedures

// src/server/routers/user.test.ts import { describe, it, expect, beforeEach } from 'vitest'; import { createCallerFactory } from '../trpc'; import { userRouter } from './user'; import { createMockContext } from '@/test/context';

describe('User Router', () => { let mockCtx: MockContext; const createCaller = createCallerFactory(userRouter);

beforeEach(() => { mockCtx = createMockContext(); });

it('should return user by id', async () => { const mockUser = { id: '1', email: 'test@example.com', name: 'Test' }; mockCtx.prisma.user.findUnique.mockResolvedValue(mockUser);

const caller = createCaller(mockCtx);
const result = await caller.getById({ id: '1' });

expect(result).toEqual(mockUser);

});

it('should reject unauthenticated create', async () => { const caller = createCaller(mockCtx); // user is null

await expect(caller.create({ email: 'new@example.com', name: 'New' }))
  .rejects.toThrow('UNAUTHORIZED');

}); });

Test Scripts

{ "scripts": { "test": "vitest", "test:run": "vitest run", "test:coverage": "vitest run --coverage" } }

  1. Deployment (Docker)

→ See [docker-node] for full guide

Multi-Stage Dockerfile

Stage 1: Dependencies

FROM node:20-alpine AS deps WORKDIR /app COPY package*.json ./ RUN npm ci --only=production && npm cache clean --force

Stage 2: Build

FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY tsconfig.json ./ COPY prisma ./prisma/ COPY src ./src/ RUN npx prisma generate RUN npm run build

Stage 3: Production

FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production

RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 -G nodejs

COPY --from=deps --chown=nodejs:nodejs /app/node_modules ./node_modules COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist COPY --from=builder --chown=nodejs:nodejs /app/prisma ./prisma COPY --from=builder --chown=nodejs:nodejs /app/node_modules/.prisma ./node_modules/.prisma

USER nodejs EXPOSE 3000

CMD ["sh", "-c", "npx prisma migrate deploy && node dist/index.js"]

Docker Compose (Development)

docker-compose.yml

version: '3.8'

services: app: build: context: . target: builder ports: - "3000:3000" environment: NODE_ENV: development DATABASE_URL: postgresql://postgres:postgres@postgres:5432/myapp volumes: - ./src:/app/src:delegated depends_on: postgres: condition: service_healthy command: npm run dev

postgres: image: postgres:15-alpine environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: myapp ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 10

volumes: postgres_data:

Commands

Development

docker-compose up # Start all docker-compose up --build # Rebuild docker-compose down -v # Stop + reset DB

Production

docker build -t myapp:latest . docker run -p 3000:3000 --env-file .env.production myapp:latest

  1. Security Checklist

Authentication

✓ Hash passwords with argon2/bcrypt ✓ Use short-lived access tokens (15min) ✓ Store refresh tokens in httpOnly cookies ✓ Validate JWT on every request ✓ Use HTTPS in production

Input Validation

✓ Validate ALL inputs with Zod ✓ Use z.coerce for query parameters ✓ Sanitize user-generated content ✓ Limit request body size

Database

✓ Use Prisma (prevents SQL injection) ✓ Never expose raw database errors ✓ Use transactions for multi-step operations ✓ Add indexes for frequent queries

Logging

✓ Redact sensitive data (passwords, tokens) ✓ Include request IDs for tracing ✓ Don't log PII in production ✓ Use structured JSON logs

  1. Error Handling

tRPC Error Codes

Code HTTP Use Case

BAD_REQUEST

400 Invalid input

UNAUTHORIZED

401 No/invalid auth

FORBIDDEN

403 No permission

NOT_FOUND

404 Resource missing

CONFLICT

409 Already exists

INTERNAL_SERVER_ERROR

500 Unexpected error

Error Handling Pattern

import { TRPCError } from '@trpc/server';

// In procedures const user = await ctx.db.user.findUnique({ where: { id } }); if (!user) { throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' }); }

// Global error formatter const t = initTRPC.context<Context>().create({ errorFormatter({ shape, error }) { return { ...shape, data: { ...shape.data, zodError: error.cause instanceof z.ZodError ? error.cause.flatten() : null, }, }; }, });

  1. Common Patterns

Cursor-Based Pagination

list: publicProcedure .input(z.object({ limit: z.number().min(1).max(100).default(10), cursor: z.string().optional(), })) .query(async ({ input, ctx }) => { const items = await ctx.db.post.findMany({ take: input.limit + 1, cursor: input.cursor ? { id: input.cursor } : undefined, orderBy: { createdAt: 'desc' }, });

let nextCursor: string | undefined;
if (items.length > input.limit) {
  nextCursor = items.pop()?.id;
}

return { items, nextCursor };

}),

Role-Based Authorization

const hasRole = (role: string) => middleware(async ({ ctx, next }) => { if (ctx.user?.role !== role) { throw new TRPCError({ code: 'FORBIDDEN' }); } return next(); });

export const adminProcedure = protectedProcedure.use(hasRole('ADMIN'));

Transactions

const result = await ctx.db.$transaction(async (tx) => { const sender = await tx.account.update({ where: { id: senderId }, data: { balance: { decrement: amount } }, });

if (sender.balance < 0) throw new Error('Insufficient funds');

await tx.account.update({ where: { id: receiverId }, data: { balance: { increment: amount } }, });

return sender; });

  1. Skill Reference Map

Task Primary Skill When to Use

Type-safe API backend-trpc Full-stack TypeScript

REST endpoints backend-trpc-openapi External clients need REST

Next.js auth backend-auth-js OAuth, sessions in Next.js

Express auth backend-passport-js JWT APIs, custom auth

Database ORM backend-prisma Any SQL database

Input validation backend-zod ALL input validation

Structured logging backend-pino Production observability

Unit testing backend-vitest tRPC, Zod, utilities

Containerization docker-node Deployment, CI/CD

  1. Quick Start Templates

Complete tRPC Router

// src/server/routers/user.ts import { z } from 'zod'; import { router, publicProcedure, protectedProcedure } from '../trpc'; import { TRPCError } from '@trpc/server';

const CreateUserSchema = z.object({ email: z.string().email(), name: z.string().min(2).max(100), });

export const userRouter = router({ getById: publicProcedure .input(z.object({ id: z.string() })) .query(async ({ input, ctx }) => { const user = await ctx.db.user.findUnique({ where: { id: input.id } }); if (!user) throw new TRPCError({ code: 'NOT_FOUND' }); return user; }),

list: publicProcedure .input(z.object({ limit: z.number().min(1).max(100).default(10), cursor: z.string().optional(), })) .query(async ({ input, ctx }) => { const items = await ctx.db.user.findMany({ take: input.limit + 1, cursor: input.cursor ? { id: input.cursor } : undefined, orderBy: { createdAt: 'desc' }, });

  let nextCursor: string | undefined;
  if (items.length > input.limit) nextCursor = items.pop()?.id;
  
  return { items, nextCursor };
}),

create: protectedProcedure .input(CreateUserSchema) .mutation(async ({ input, ctx }) => { return ctx.db.user.create({ data: input }); }),

update: protectedProcedure .input(z.object({ id: z.string(), name: z.string().min(2).optional(), })) .mutation(async ({ input, ctx }) => { const { id, ...data } = input; return ctx.db.user.update({ where: { id }, data }); }),

delete: protectedProcedure .input(z.object({ id: z.string() })) .mutation(async ({ input, ctx }) => { await ctx.db.user.delete({ where: { id: input.id } }); return { success: true }; }), });

Express Server with tRPC

// src/index.ts import express from 'express'; import cors from 'cors'; import { createExpressMiddleware } from '@trpc/server/adapters/express'; import { appRouter } from './server/routers/_app'; import { createContext } from './server/context'; import { logger } from './lib/logger'; import { requestLogger } from './middleware/logging';

const app = express();

app.use(cors()); app.use(express.json()); app.use(requestLogger);

app.get('/health', async (req, res) => { try { await prisma.$queryRawSELECT 1; res.json({ status: 'healthy' }); } catch { res.status(503).json({ status: 'unhealthy' }); } });

app.use('/trpc', createExpressMiddleware({ router: appRouter, createContext, }));

const port = process.env.PORT || 3000; app.listen(port, () => { logger.info({ port }, 'Server started'); });

External Resources

For latest API of any library → use context7 skill

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

code-analyzer

No summary provided by upstream source.

Repository SourceNeeds Review
General

frontend-magic-ui

No summary provided by upstream source.

Repository SourceNeeds Review
General

frontend-google-fonts

No summary provided by upstream source.

Repository SourceNeeds Review
General

figma-design-extraction

No summary provided by upstream source.

Repository SourceNeeds Review