xiaohongshu
xiaohongshu
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 ...
Full skill instructions
Unified decision framework for TypeScript backend development.
Stack: Node.js · TypeScript · tRPC/Express · Prisma · Zod · Vitest · Docker
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]
# 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
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
→ 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);
→ 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 |
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
→ 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;
},
},
});
→ 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+ |
→ See [backend-prisma] for full guide
// 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;
// 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
}
npx prisma migrate dev --name init # Development
npx prisma migrate deploy # Production
npx prisma generate # Regenerate client
npx prisma studio # GUI viewer
→ See [backend-zod] for full guide
// 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>;
// 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 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 });
}),
});
→ See [backend-pino] for full guide
// 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,
},
});
// 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();
}
// ❌ String interpolation
logger.info(`User ${userId} logged in from ${ip}`);
// ✅ Structured objects
logger.info({ userId, ip, action: 'login' }, 'User logged in');
→ See [backend-vitest] for full guide
// 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'],
},
},
});
// 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,
});
// 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');
});
});
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}
→ See [docker-node] for full guide
# 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.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:
# 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
✓ 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
✓ Validate ALL inputs with Zod
✓ Use z.coerce for query parameters
✓ Sanitize user-generated content
✓ Limit request body size
✓ Use Prisma (prevents SQL injection)
✓ Never expose raw database errors
✓ Use transactions for multi-step operations
✓ Add indexes for frequent queries
✓ Redact sensitive data (passwords, tokens)
✓ Include request IDs for tracing
✓ Don't log PII in production
✓ Use structured JSON logs
| 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 |
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,
},
};
},
});
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 };
}),
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'));
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;
});
| 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 |
// 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 };
}),
});
// 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');
});
For latest API of any library → use context7 skill
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
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...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
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...
Display project status, active tracks, and next actions
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.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience