Back to list
petbrains

backend-master

by petbrains

Document-Driven Development framework for Claude Code — structured specs, TDD cycles, feedback loops, and skills system

6🍴 1📅 Jan 24, 2026

SKILL.md


name: backend-master description: 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. allowed-tools: Read, Edit, Write, Bash (*)

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
{
  "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"]
}
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'],
  },
})
ScenarioRecommendation
Internal TypeScript clientsPure tRPC
Third-party integrationstRPC + OpenAPI
Public API documentationtRPC + 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);
}));
FeatureAuth.jsPassport.js
Best forNext.jsExpress
OAuth setupMinimalManual
JWT supportBuilt-inpassport-jwt
Session storageJWT/DBManual
ServerlessYesLimited
Strategies~20500+

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

CodeHTTPUse Case
BAD_REQUEST400Invalid input
UNAUTHORIZED401No/invalid auth
FORBIDDEN403No permission
NOT_FOUND404Resource missing
CONFLICT409Already exists
INTERNAL_SERVER_ERROR500Unexpected 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

TaskPrimary SkillWhen to Use
Type-safe APIbackend-trpcFull-stack TypeScript
REST endpointsbackend-trpc-openapiExternal clients need REST
Next.js authbackend-auth-jsOAuth, sessions in Next.js
Express authbackend-passport-jsJWT APIs, custom auth
Database ORMbackend-prismaAny SQL database
Input validationbackend-zodALL input validation
Structured loggingbackend-pinoProduction observability
Unit testingbackend-vitesttRPC, Zod, utilities
Containerizationdocker-nodeDeployment, 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

For latest API of any library → use context7 skill

Score

Total Score

75/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon