Skip to content
backend-master logo

Backend Master Skill

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 ...

SKILL.md

Full skill instructions

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

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: '[email protected]', 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: '[email protected]', 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

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View