Claude-skill-registry backend-master
Master skill for TypeScript backend development. Decision framework for APIs (tRPC/REST), authentication (Auth.js/Passport), database (Prisma), validation (Zod), logging (Pino), testing (Vitest), and deployment (Docker). Routes to specialized skills for implementation. Use as entry point for any backend task.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/backend-master" ~/.claude/skills/majiayu000-claude-skill-registry-backend-master && rm -rf "$T"
skills/data/backend-master/SKILL.mdBackend 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
2. 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 |
3. 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+ |
4. 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
5. 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 }); }), });
6. 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');
7. 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" } }
8. 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
9. 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
10. Error Handling
tRPC Error Codes
| Code | HTTP | Use Case |
|---|---|---|
| 400 | Invalid input |
| 401 | No/invalid auth |
| 403 | No permission |
| 404 | Resource missing |
| 409 | Already exists |
| 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, }, }; }, });
11. 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; });
12. 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 |
13. 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.$queryRaw`SELECT 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
- tRPC: https://trpc.io/docs
- Prisma: https://prisma.io/docs
- Zod: https://zod.dev
- Auth.js: https://authjs.dev
- Passport.js: https://passportjs.org
- Pino: https://getpino.io
- Vitest: https://vitest.dev
- Docker: https://docs.docker.com
For latest API of any library → use context7 skill