---
id: daily-fullstack-dev
name: "fullstack-dev"
url: https://skills.yangsir.net/skill/daily-fullstack-dev
author: minimax-ai
domain: ai-app-building-integration
tags: ["backend", "frontend", "web-development", "project-scaffolding", "best-practices"]
install_count: 1900
rating: 4.30 (20 reviews)
github: https://github.com/minimax-ai/skills
---

# fullstack-dev

> 全栈开发最佳实践，从需求收集到项目脚手架的标准化工作流程，AI Agent Skill，提升工作效率和自动化能力

**Stats**: 1,900 installs · 4.3/5 (20 reviews)

## Before / After 对比

### 项目启动效率对比

**Before**:

手动选择技术栈、配置开发环境、搭建项目结构，容易遗漏关键配置，启动一个项目需要 1-2 天

**After**:

按照标准化流程收集需求、选择栈、生成脚手架，2 小时完成项目初始化和配置

| Metric | Before | After | Change |
|---|---|---|---|
| 项目启动时间 | 1440分钟 | 120分钟 | -92% |

## Readme

# fullstack-dev

# Full-Stack Development Practices

## MANDATORY WORKFLOW — Follow These Steps In Order

**When this skill is triggered, you MUST follow this workflow before writing any code.**

### Step 0: Gather Requirements

Before scaffolding anything, ask the user to clarify (or infer from context):

- **Stack**: Language/framework for backend and frontend (e.g., Express + React, Django + Vue, Go + HTMX)

- **Service type**: API-only, full-stack monolith, or microservice?

- **Database**: SQL (PostgreSQL, SQLite, MySQL) or NoSQL (MongoDB, Redis)?

- **Integration**: REST, GraphQL, tRPC, or gRPC?

- **Real-time**: Needed? If yes — SSE, WebSocket, or polling?

- **Auth**: Needed? If yes — JWT, session, OAuth, or third-party (Clerk, Auth.js)?

If the user has already specified these in their request, skip asking and proceed.

### Step 1: Architectural Decisions

Based on requirements, make and state these decisions before coding:

Decision
Options
Reference

Project structure
Feature-first (recommended) vs layer-first
[Section 1](#1-project-structure--layering-critical)

API client approach
Typed fetch / React Query / tRPC / OpenAPI codegen
[Section 5](#5-api-client-patterns-medium)

Auth strategy
JWT + refresh / session / third-party
[Section 6](#6-authentication--middleware-high)

Real-time method
Polling / SSE / WebSocket
[Section 11](#11-real-time-patterns-medium)

Error handling
Typed error hierarchy + global handler
[Section 3](#3-error-handling--resilience-high)

Briefly explain each choice (1 sentence per decision).

### Step 2: Scaffold with Checklist

Use the appropriate checklist below. Ensure ALL checked items are implemented — do not skip any.

### Step 3: Implement Following Patterns

Write code following the patterns in this document. Reference specific sections as you implement each part.

### Step 4: Test & Verify

After implementation, run these checks before claiming completion:

- **Build check**: Ensure both backend and frontend compile without errors

```
# Backend
cd server && npm run build
# Frontend
cd client && npm run build

```

- **Start & smoke test**: Start the server, verify key endpoints return expected responses

```
# Start server, then test
curl http://localhost:3000/health
curl http://localhost:3000/api/<resource>

```

- **Integration check**: Verify frontend can connect to backend (CORS, API base URL, auth flow)

- **Real-time check** (if applicable): Open two browser tabs, verify changes sync

If any check fails, fix the issue before proceeding.

### Step 5: Handoff Summary

Provide a brief summary to the user:

- **What was built**: List of implemented features and endpoints

- **How to run**: Exact commands to start backend and frontend

- **What's missing / next steps**: Any deferred items, known limitations, or recommended improvements

- **Key files**: List the most important files the user should know about

## Scope

**USE this skill when:**

- Building a full-stack application (backend + frontend)

- Scaffolding a new backend service or API

- Designing service layers and module boundaries

- Implementing database access, caching, or background jobs

- Writing error handling, logging, or configuration management

- Reviewing backend code for architectural issues

- Hardening for production

- Setting up API clients, auth flows, file uploads, or real-time features

**NOT for:**

- Pure frontend/UI concerns (use your frontend framework's docs)

- Pure database schema design without backend context

## Quick Start — New Backend Service Checklist

-  Project scaffolded with **feature-first** structure

-  Configuration **centralized**, env vars **validated at startup** (fail fast)

-  **Typed error hierarchy** defined (not generic `Error`)

-  **Global error handler** middleware

-  **Structured JSON logging** with request ID propagation

-  Database: **migrations** set up, **connection pooling** configured

-  **Input validation** on all endpoints (Zod / Pydantic / Go validator)

-  **Authentication middleware** in place

-  **Health check** endpoints (`/health`, `/ready`)

-  **Graceful shutdown** handling (SIGTERM)

-  **CORS** configured (explicit origins, not `*`)

-  **Security headers** (helmet or equivalent)

-  `.env.example` committed (no real secrets)

## Quick Start — Frontend-Backend Integration Checklist

-  **API client** configured (typed fetch wrapper, React Query, tRPC, or OpenAPI generated)

-  **Base URL** from environment variable (not hardcoded)

-  **Auth token** attached to requests automatically (interceptor / middleware)

-  **Error handling** — API errors mapped to user-facing messages

-  **Loading states** handled (skeleton/spinner, not blank screen)

-  **Type safety** across the boundary (shared types, OpenAPI, or tRPC)

-  **CORS** configured with explicit origins (not `*` in production)

-  **Refresh token** flow implemented (httpOnly cookie + transparent retry on 401)

## Quick Navigation

Need to…
Jump to

Organize project folders
[1. Project Structure](#1-project-structure--layering-critical)

Manage config + secrets
[2. Configuration](#2-configuration--environment-critical)

Handle errors properly
[3. Error Handling](#3-error-handling--resilience-high)

Write database code
[4. Database Access Patterns](#4-database-access-patterns-high)

Set up API client from frontend
[5. API Client Patterns](#5-api-client-patterns-medium)

Add auth middleware
[6. Auth & Middleware](#6-authentication--middleware-high)

Set up logging
[7. Logging & Observability](#7-logging--observability-medium-high)

Add background jobs
[8. Background Jobs](#8-background-jobs--async-medium)

Implement caching
[9. Caching](#9-caching-patterns-medium)

Upload files (presigned URL, multipart)
[10. File Upload Patterns](#10-file-upload-patterns-medium)

Add real-time features (SSE, WebSocket)
[11. Real-Time Patterns](#11-real-time-patterns-medium)

Handle API errors in frontend UI
[12. Cross-Boundary Error Handling](#12-cross-boundary-error-handling-medium)

Harden for production
[13. Production Hardening](#13-production-hardening-medium)

Design API endpoints
[API Design](https://github.com/minimax-ai/skills/blob/HEAD/skills/fullstack-dev/references/api-design.md)

Design database schema
[Database Schema](https://github.com/minimax-ai/skills/blob/HEAD/skills/fullstack-dev/references/db-schema.md)

Auth flow (JWT, refresh, Next.js SSR, RBAC)
[references/auth-flow.md](https://github.com/minimax-ai/skills/blob/HEAD/skills/fullstack-dev/references/auth-flow.md)

CORS, env vars, environment management
[references/environment-management.md](https://github.com/minimax-ai/skills/blob/HEAD/skills/fullstack-dev/references/environment-management.md)

## Core Principles (7 Iron Rules)

```
1. ✅ Organize by FEATURE, not by technical layer
2. ✅ Controllers never contain business logic
3. ✅ Services never import HTTP request/response types
4. ✅ All config from env vars, validated at startup, fail fast
5. ✅ Every error is typed, logged, and returns consistent format
6. ✅ All input validated at the boundary — trust nothing from client
7. ✅ Structured JSON logging with request ID — not console.log

```

## 1. Project Structure & Layering (CRITICAL)

### Feature-First Organization

```
✅ Feature-first                    ❌ Layer-first
src/                                src/
  orders/                             controllers/
    order.controller.ts                 order.controller.ts
    order.service.ts                    user.controller.ts
    order.repository.ts               services/
    order.dto.ts                        order.service.ts
    order.test.ts                       user.service.ts
  users/                              repositories/
    user.controller.ts                  ...
    user.service.ts
  shared/
    database/
    middleware/

```

### Three-Layer Architecture

```
Controller (HTTP) → Service (Business Logic) → Repository (Data Access)

```

Layer
Responsibility
❌ Never

Controller
Parse request, validate, call service, format response
Business logic, DB queries

Service
Business rules, orchestration, transaction mgmt
HTTP types (req/res), direct DB

Repository
Database queries, external API calls
Business logic, HTTP types

### Dependency Injection (All Languages)

**TypeScript:**

```
class OrderService {
  constructor(
    private readonly orderRepo: OrderRepository,    // ✅ injected interface
    private readonly emailService: EmailService,
  ) {}
}

```

**Python:**

```
class OrderService:
    def __init__(self, order_repo: OrderRepository, email_service: EmailService):
        self.order_repo = order_repo                 # ✅ injected
        self.email_service = email_service

```

**Go:**

```
type OrderService struct {
    orderRepo    OrderRepository                      // ✅ interface
    emailService EmailService
}

func NewOrderService(repo OrderRepository, email EmailService) *OrderService {
    return &OrderService{orderRepo: repo, emailService: email}
}

```

## 2. Configuration & Environment (CRITICAL)

### Centralized, Typed, Fail-Fast

**TypeScript:**

```
const config = {
  port: parseInt(process.env.PORT || '3000', 10),
  database: { url: requiredEnv('DATABASE_URL'), poolSize: intEnv('DB_POOL_SIZE', 10) },
  auth: { jwtSecret: requiredEnv('JWT_SECRET'), expiresIn: process.env.JWT_EXPIRES_IN || '1h' },
} as const;

function requiredEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing required env var: ${name}`);  // fail fast
  return value;
}

```

**Python:**

```
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str                        # required — app won't start without it
    jwt_secret: str                          # required
    port: int = 3000                         # optional with default
    db_pool_size: int = 10
    class Config:
        env_file = ".env"

settings = Settings()                        # fails fast if DATABASE_URL missing

```

### Rules

```
✅ All config via environment variables (Twelve-Factor)
✅ Validate required vars at startup — fail fast
✅ Type-cast at config layer, not at usage sites
✅ Commit .env.example with dummy values

❌ Never hardcode secrets, URLs, or credentials
❌ Never commit .env files
❌ Never scatter process.env / os.environ throughout code

```

## 3. Error Handling & Resilience (HIGH)

### Typed Error Hierarchy

```
// Base (TypeScript)
class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number,
    public readonly isOperational: boolean = true,
  ) { super(message); }
}
class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(`${resource} not found: ${id}`, 'NOT_FOUND', 404);
  }
}
class ValidationError extends AppError {
  constructor(public readonly errors: FieldError[]) {
    super('Validation failed', 'VALIDATION_ERROR', 422);
  }
}

```

```
# Base (Python)
class AppError(Exception):
    def __init__(self, message: str, code: str, status_code: int):
        self.message, self.code, self.status_code = message, code, status_code

class NotFoundError(AppError):
    def __init__(self, resource: str, id: str):
        super().__init__(f"{resource} not found: {id}", "NOT_FOUND", 404)

```

### Global Error Handler

```
// TypeScript (Express)
app.use((err, req, res, next) => {
  if (err instanceof AppError && err.isOperational) {
    return res.status(err.statusCode).json({
      title: err.code, status: err.statusCode,
      detail: err.message, request_id: req.id,
    });
  }
  logger.error('Unexpected error', { error: err.message, stack: err.stack, request_id: req.id });
  res.status(500).json({ title: 'Internal Error', status: 500, request_id: req.id });
});

```

### Rules

```
✅ Typed, domain-specific error classes
✅ Global error handler catches everything
✅ Operational errors → structured response
✅ Programming errors → log + generic 500
✅ Retry transient failures with exponential backoff

❌ Never catch and ignore errors silently
❌ Never return stack traces to client
❌ Never throw generic Error('something')

```

## 4. Database Access Patterns (HIGH)

### Migrations Always

```
# TypeScript (Prisma)           # Python (Alembic)              # Go (golang-migrate)
npx prisma migrate dev          alembic revision --autogenerate  migrate -source file://migrations
npx prisma migrate deploy       alembic upgrade head             migrate -database $DB up

```

```
✅ Schema changes via migrations, never manual SQL
✅ Migrations must be reversible
✅ Review migration SQL before production
❌ Never modify production schema manually

```

### N+1 Prevention

```
// ❌ N+1: 1 query + N queries
const orders = await db.order.findMany();
for (const o of orders) { o.items = await db.item.findMany({ where: { orderId: o.id } }); }

// ✅ Single JOIN query
const orders = await db.order.findMany({ include: { items: true } });

```

### Transactions for Multi-Step Writes

```
await db.$transaction(async (tx) => {
  const order = await tx.order.create({ data: orderData });
  await tx.inventory.decrement({ productId, quantity });
  await tx.payment.create({ orderId: order.id, amount });
});

```

### Connection Pooling

Pool size = `(CPU cores × 2) + spindle_count` (start with 10-20). Always set connection timeout. Use PgBouncer for serverless.

## 5. API Client Patterns (MEDIUM)

The "glue layer" between frontend and backend. Choose the approach that fits your team and stack.

### Option A: Typed Fetch Wrapper (Simple, No Dependencies)

```
// lib/api-client.ts
const BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';

class ApiError extends Error {
  constructor(public status: number, public body: any) {
    super(body?.detail || body?.message || `API error ${status}`);
  }
}

async function api<T>(path: string, options: RequestInit = {}): Promise<T> {
  const token = getAuthToken();  // from cookie / memory / context

  const res = await fetch(`${BASE_URL}${path}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      ...options.headers,
    },
  });

  if (!res.ok) {
    const body = await res.json().catch(() => null);
    throw new ApiError(res.status, body);
  }

  if (res.status === 204) return undefined as T;
  return res.json();
}

export const apiClient = {
  get: <T>(path: string) => api<T>(path),
  post: <T>(path: string, data: unknown) => api<T>(path, { method: 'POST', body: JSON.stringify(data) }),
  put: <T>(path: string, data: unknown) => api<T>(path, { method: 'PUT', body: JSON.stringify(data) }),
  patch: <T>(path: string, data: unknown) => api<T>(path, { method: 'PATCH', body: JSON.stringify(data) }),
  delete: <T>(path: string) => api<T>(path, { method: 'DELETE' }),
};

```

### Option B: React Query + Typed Client (Recommended for React)

```
// hooks/use-orders.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/lib/api-client';

interface Order { id: string; total: number; status: string; }
interface CreateOrderInput { items: { productId: string; quantity: number }[] }

export function useOrders() {
  return useQuery({
    queryKey: ['orders'],
    queryFn: () => apiClient.get<{ data: Order[] }>('/api/orders'),
    staleTime: 1000 * 60,  // 1 min
  });
}

export function useCreateOrder() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (data: CreateOrderInput) =>
      apiClient.post<{ data: Order }>('/api/orders', data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['orders'] });
    },
  });
}

// Usage in component:
function OrdersPage() {
  const { data, isLoading, error } = useOrders();
  const createOrder = useCreateOrder();
  if (isLoading) return <Skeleton />;
  if (error) return <ErrorBanner error={error} />;
  // ...
}

```

### Option C: tRPC (Same Team Owns Both Sides)

```
// server: trpc/router.ts
export const appRouter = router({
  orders: router({
    list: publicProcedure.query(async () => {
      return db.order.findMany({ include: { items: true } });
    }),
    create: protectedProcedure
      .input(z.object({ items: z.array(orderItemSchema) }))
      .mutation(async ({ input, ctx }) => {
        return orderService.create(ctx.user.id, input);
      }),
  }),
});
export type AppRouter = typeof appRouter;

// client: automatic type safety, no code generation
const { data } = trpc.orders.list.useQuery();
const createOrder = trpc.orders.create.useMutation();

```

### Option D: OpenAPI Generated Client (Public / Multi-Consumer APIs)

```
npx openapi-typescript-codegen \
  --input http://localhost:3001/api/openapi.json \
  --output src/generated/api \
  --client axios

```

### Decision: Which API Client?

Approach
When
Type Safety
Effort

Typed fetch wrapper
Simple apps, small teams
Manual types
Low

React Query + fetch
React apps, server state
Manual types
Medium

tRPC
Same team, TypeScript both sides
Automatic
Low

OpenAPI generated
Public API, multi-consumer
Automatic
Medium

GraphQL codegen
GraphQL APIs
Automatic
Medium

## 6. Authentication & Middleware (HIGH)

**Full reference:** [references/auth-flow.md](https://github.com/minimax-ai/skills/blob/HEAD/skills/fullstack-dev/references/auth-flow.md) — JWT bearer flow, automatic token refresh, Next.js server-side auth, RBAC pattern, backend middleware order.

### Standard Middleware Order

```
Request → 1.RequestID → 2.Logging → 3.CORS → 4.RateLimit → 5.BodyParse
       → 6.Auth → 7.Authz → 8.Validation → 9.Handler → 10.ErrorHandler → Response

```

### JWT Rules

```
✅ Short expiry access token (15min) + refresh token (server-stored)
✅ Minimal claims: userId, roles (not entire user object)
✅ Rotate signing keys periodically

❌ Never store tokens in localStorage (XSS risk)
❌ Never pass tokens in URL query params

```

### RBAC Pattern

```
function authorize(...roles: Role[]) {
  return (req, res, next) => {
    if (!req.user) throw new UnauthorizedError();
    if (!roles.some(r => req.user.roles.includes(r))) throw new ForbiddenError();
    next();
  };
}
router.delete('/users/:id', authenticate, authorize('admin'), deleteUser);

```

### Auth Token Automatic Refresh

```
// lib/api-client.ts — transparent refresh on 401
async function apiWithRefresh<T>(path: string, options: RequestInit = {}): Promise<T> {
  try {
    return await api<T>(path, options);
  } catch (err) {
    if (err instanceof ApiError && err.status === 401) {
      const refreshed = await api<{ accessToken: string }>('/api/auth/refresh', {
        method: 'POST',
        credentials: 'include',  // send httpOnly cookie
      });
      setAuthToken(refreshed.accessToken);
      return api<T>(path, options);  // retry
    }
    throw err;
  }
}

```

## 7. Logging & Observability (MEDIUM-HIGH)

### Structured JSON Logging

```
// ✅ Structured — parseable, filterable, alertable
logger.info('Order created', {
  orderId: order.id, userId: user.id, total: order.total,
  items: order.items.length, duration_ms: Date.now() - startTime,
});
// Output: {"level":"info","msg":"Order created","orderId":"ord_123",...}

// ❌ Unstructured — useless at scale
console.log(`Order created for user ${user.id} with total ${order.total}`);

```

### Log Levels

Level
When
Production?

error
Requires immediate attention
✅ Always

warn
Unexpected but handled
✅ Always

info
Normal operations, audit trail
✅ Always

debug
Dev troubleshooting
❌ Dev only

### Rules

```
✅ Request ID in every log entry (propagated via middleware)
✅ Log at layer boundaries (request in, response out, external call)
❌ Never log passwords, tokens, PII, or secrets
❌ Never use console.log in production code

```

## 8. Background Jobs & Async (MEDIUM)

### Rules

```
✅ All jobs must be IDEMPOTENT (same job running twice = same result)
✅ Failed jobs → retry (max 3) → dead letter queue → alert
✅ Workers run as SEPARATE processes (not threads in API server)

❌ Never put long-running tasks in request handlers
❌ Never assume job runs exactly once

```

### Idempotent Job Pattern

```
async function processPayment(data: { orderId: string }) {
  const order = await orderRepo.findById(data.orderId);
  if (order.paymentStatus === 'completed') return;  // already processed
  await paymentGateway.charge(order);
  await orderRepo.updatePaymentStatus(order.id, 'completed');
}

```

## 9. Caching Patterns (MEDIUM)

### Cache-Aside (Lazy Loading)

```
async function getUser(id: string): Promise<User> {
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);

  const user = await userRepo.findById(id);
  if (!user) throw new NotFoundError('User', id);

  await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 900);  // 15min TTL
  return user;
}

```

### Rules

```
✅ ALWAYS set TTL — never cache without expiry
✅ Invalidate on write (delete cache key after update)
✅ Use cache for reads, never for authoritative state

❌ Never cache without TTL (stale data is worse than slow data)

```

Data Type
Suggested TTL

User profile
5-15 min

Product catalog
1-5 min

Config / feature flags
30-60 sec

Session
Match session duration

## 10. File Upload Patterns (MEDIUM)

### Option A: Presigned URL (Recommended for Large Files)

```
Client → GET /api/uploads/presign?filename=photo.jpg&type=image/jpeg
Server → { uploadUrl: "https://s3.../presigned", fileKey: "uploads/abc123.jpg" }
Client → PUT uploadUrl (direct to S3, bypasses your server)
Client → POST /api/photos { fileKey: "uploads/abc123.jpg" }  (save reference)

```

**Backend:**

```
app.get('/api/uploads/presign', authenticate, async (req, res) => {
  const { filename, type } = req.query;
  const key = `uploads/${crypto.randomUUID()}-${filename}`;
  const url = await s3.getSignedUrl('putObject', {
    Bucket: process.env.S3_BUCKET, Key: key,
    ContentType: type, Expires: 300,  // 5 min
  });
  res.json({ uploadUrl: url, fileKey: key });
});

```

**Frontend:**

```
async function uploadFile(file: File) {
  const { uploadUrl, fileKey } = await apiClient.get<PresignResponse>(
    `/api/uploads/presign?filename=${file.name}&type=${file.type}`
  );
  await fetch(uploadUrl, { method: 'PUT', body: file, headers: { 'Content-Type': file.type } });
  return apiClient.post('/api/photos', { fileKey });
}

```

### Option B: Multipart (Small Files < 10MB)

```
// Frontend
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'Profile photo');
const res = await fetch('/api/upload', { method: 'POST', body: formData });
// Note: do NOT set Content-Type header — browser sets boundary automatically

```

### Decision

Method
File Size
Server Load
Complexity

Presigned URL
Any (recommended > 5MB)
None (direct to storage)
Medium

Multipart
< 10MB
High (streams through server)
Low

Chunked / Resumable
> 100MB
Medium
High

## 11. Real-Time Patterns (MEDIUM)

### Option A: Server-Sent Events (SSE) — One-Way Server → Client

Best for: notifications, live feeds, streaming AI responses.

**Backend (Express):**

```
app.get('/api/events', authenticate, (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
  });
  const send = (event: string, data: unknown) => {
    res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
  };
  const unsubscribe = eventBus.subscribe(req.user.id, (event) => {
    send(event.type, event.payload);
  });
  req.on('close', () => unsubscribe());
});

```

**Frontend:**

```
function useServerEvents(userId: string) {
  useEffect(() => {
    const source = new EventSource(`/api/events?userId=${userId}`);
    source.addEventListener('notification', (e) => {
      showToast(JSON.parse(e.data).message);
    });
    source.onerror = () => { source.close(); setTimeout(() => /* reconnect */, 3000); };
    return () => source.close();
  }, [userId]);
}

```

### Option B: WebSocket — Bidirectional

Best for: chat, collaborative editing, gaming.

**Backend (ws library):**

```
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ server: httpServer, path: '/ws' });
wss.on('connection', (ws, req) => {
  const userId = authenticateWs(req);
  if (!userId) { ws.close(4001, 'Unauthorized'); return; }
  ws.on('message', (raw) => handleMessage(userId, JSON.parse(raw.toString())));
  ws.on('close', () => cleanupUser(userId));
  const interval = setInterval(() => ws.ping(), 30000);
  ws.on('pong', () => { /* alive */ });
  ws.on('close', () => clearInterval(interval));
});

```

**Frontend:**

```
function useWebSocket(url: string) {
  const [ws, setWs] = useState<WebSocket | null>(null);
  useEffect(() => {
    const socket = new WebSocket(url);
    socket.onopen = () => setWs(socket);
    socket.onclose = () => setTimeout(() => /* reconnect */, 3000);
    return () => socket.close();
  }, [url]);
  const send = useCallback((data: unknown) => ws?.send(JSON.stringify(data)), [ws]);
  return { ws, send };
}

```

### Option C: Polling (Simplest, No Infrastructure)

```
function useOrderStatus(orderId: string) {
  return useQuery({
    queryKey: ['order-status', orderId],
    queryFn: () => apiClient.get<Order>(`/api/orders/${orderId}`),
    refetchInterval: (query) => {
      if (query.state.data?.status === 'completed') return false;
      return 5000;
    },
  });
}

```

### Decision

Method
Direction
Complexity
When

Polling
Client → Server
Low
Simple status checks, < 10 clients

SSE
Server → Client
Medium
Notifications, feeds, AI streaming

WebSocket
Bidirectional
High
Chat, collaboration, gaming

## 12. Cross-Boundary Error Handling (MEDIUM)

### API Error → User-Facing Message

```
// lib/error-handler.ts
export function getErrorMessage(error: unknown): string {
  if (error instanceof ApiError) {
    switch (error.status) {
      case 401: return 'Please log in to continue.';
      case 403: return 'You don\'t have permission to do this.';
      case 404: return 'The item you\'re looking for doesn\'t exist.';
      case 409: return 'This conflicts with an existing item.';
      case 422:
        const fields = error.body?.errors;
        if (fields?.length) return fields.map((f: any) => f.message).join('. ');
        return 'Please check your input.';
      case 429: return 'Too many requests. Please wait a moment.';
      default: return 'Something went wrong. Please try again.';
    }
  }
  if (error instanceof TypeError && error.message === 'Failed to fetch') {
    return 'Cannot connect to server. Check your internet connection.';
  }
  return 'An unexpected error occurred.';
}

```

### React Query Global Error Handler

```
const queryClient = new QueryClient({
  defaultOptions: {
    mutations: { onError: (error) => toast.error(getErrorMessage(error)) },
    queries: {
      retry: (failureCount, error) => {
        if (error instanceof ApiError && error.status < 500) return false;
        return failureCount < 3;
      },
    },
  },
});

```

### Rules

```
✅ Map every API error code to a human-readable message
✅ Show field-level validation errors next to form inputs
✅ Auto-retry on 5xx (max 3, with backoff), never on 4xx
✅ Redirect to login on 401 (after refresh attempt fails)
✅ Show "offline" banner when fetch fails with TypeError

❌ Never show raw API error messages to users ("NullPointerException")
❌ Never silently swallow errors (show toast or log)
❌ Never retry 4xx errors (client is wrong, retrying won't help)

```

### Integration Decision Tree

```
Same team owns frontend + backend?
│
├─ YES, both TypeScript
│   └─ tRPC (end-to-end type safety, zero codegen)
│
├─ YES, different languages
│   └─ OpenAPI spec → generated client (type safety via codegen)
│
├─ NO, public API
│   └─ REST + OpenAPI → generated SDKs for consumers
│
└─ Complex data needs, multiple frontends
    └─ GraphQL + codegen (flexible queries per client)

Real-time needed?
│
├─ Server → Client only (notifications, feeds, AI streaming)
│   └─ SSE (simplest, auto-reconnect, works through proxies)
│
├─ Bidirectional (chat, collaboration)
│   └─ WebSocket (need heartbeat + reconnection logic)
│
└─ Simple status polling (< 10 clients)
    └─ React Query refetchInterval (no infrastructure needed)

```

## 13. Production Hardening (MEDIUM)

### Health Checks

```
app.get('/health', (req, res) => res.json({ status: 'ok' }));           // liveness
app.get('/ready', async (req, res) => {                                   // readiness
  const checks = {
    database: await checkDb(), redis: await checkRedis(), 
  };
  const ok = Object.values(checks).every(c => c.status === 'ok');
  res.status(ok ? 200 : 503).json({ status: ok ? 'ok' : 'degraded', checks });
});

```

### Graceful Shutdown

```
process.on('SIGTERM', async () => {
  logger.info('SIGTERM received');
  server.close();              // stop new connections
  await drainConnections();    // finish in-flight
  await closeDatabase();
  process.exit(0);
});

```

### Security Checklist

```
✅ CORS: explicit origins (never '*' in production)
✅ Security headers (helmet / equivalent)
✅ Rate limiting on public endpoints
✅ Input validation on ALL endpoints (trust nothing)
✅ HTTPS enforced
❌ Never expose internal errors to clients

```

## Anti-Patterns

#
❌ Don't
✅ Do Instead

1
Business logic in routes/controllers
Move to service layer

2
`process.env` scattered everywhere
Centralized typed config

3
`console.log` for logging
Structured JSON logger

4
Generic `Error('oops')`
Typed error hierarchy

5
Direct DB calls in controllers
Repository pattern

6
No input validation
Validate at boundary (Zod/Pydantic)

7
Catching errors silently
Log + rethrow or return error

8
No health check endpoints
`/health` + `/ready`

9
Hardcoded config/secrets
Environment variables

10
No graceful shutdown
Handle SIGTERM properly

11
Hardcode API URL in frontend
Environment variable (`NEXT_PUBLIC_API_URL`)

12
Store JWT in localStorage
Memory + httpOnly refresh cookie

13
Show raw API errors to users
Map to human-readable messages

14
Retry 4xx errors
Only retry 5xx (server failures)

15
Skip loading states
Skeleton/spinner while fetching

16
Upload large files through API server
Presigned URL → direct to S3

17
Poll for real-time data
SSE or WebSocket

18
Duplicate types frontend + backend
Shared types, tRPC, or OpenAPI codegen

## Common Issues

### Issue 1: "Where does this business rule go?"

**Rule:** If it involves HTTP (request parsing, status codes, headers) → controller. If it involves business decisions (pricing, permissions, rules) → service. If it touches the database → repository.

### Issue 2: "Service is getting too big"

**Symptom:** One service file > 500 lines with 20+ methods.

**Fix:** Split by sub-domain. `OrderService` → `OrderCreationService` + `OrderFulfillmentService` + `OrderQueryService`. Each focused on one workflow.

### Issue 3: "Tests are slow because they hit the database"

**Fix:** Unit tests mock the repository layer (fast). Integration tests use test containers or transaction rollback (real DB, still fast). Never mock the service layer in integration tests.

## Reference Documents

This skill includes deep-dive references for specialized topics. Read the relevant reference when you need detailed guidance.

Need to…
Reference

Write backend tests (unit, integration, e2e, contract, performance)
[references/testing-strategy.md](https://github.com/minimax-ai/skills/blob/HEAD/skills/fullstack-dev/references/testing-strategy.md)

Validate a release before deployment (6-gate checklist)
[references/release-checklist.md](https://github.com/minimax-ai/skills/blob/HEAD/skills/fullstack-dev/references/release-checklist.md)

Choose a tech stack (language, framework, database, infra)
[references/technology-selection.md](https://github.com/minimax-ai/skills/blob/HEAD/skills/fullstack-dev/references/technology-selection.md)

Build with Django / DRF (models, views, serializers, admin)
[references/django-best-practices.md](https://github.com/minimax-ai/skills/blob/HEAD/skills/fullstack-dev/references/django-best-practices.md)

Design REST/GraphQL/gRPC endpoints (URLs, status codes, pagination)
[references/api-design.md](https://github.com/minimax-ai/skills/blob/HEAD/skills/fullstack-dev/references/api-design.md)

Design database schema, indexes, migrations, multi-tenancy
[references/db-schema.md](https://github.com/minimax-ai/skills/blob/HEAD/skills/fullstack-dev/references/db-schema.md)

Auth flow (JWT bearer, token refresh, Next.js SSR, RBAC, middleware order)
[references/auth-flow.md](https://github.com/minimax-ai/skills/blob/HEAD/skills/fullstack-dev/references/auth-flow.md)

CORS config, env vars per environment, common CORS issues
[references/environment-management.md](https://github.com/minimax-ai/skills/blob/HEAD/skills/fullstack-dev/references/environment-management.md)

Weekly Installs281Repository[minimax-ai/skills](https://github.com/minimax-ai/skills)GitHub Stars5.5KFirst Seen8 days agoSecurity Audits[Gen Agent Trust HubPass](/minimax-ai/skills/fullstack-dev/security/agent-trust-hub)[SocketPass](/minimax-ai/skills/fullstack-dev/security/socket)[SnykPass](/minimax-ai/skills/fullstack-dev/security/snyk)Installed oncodex273opencode273cursor270gemini-cli269github-copilot269kimi-cli268

---
*Source: https://skills.yangsir.net/skill/daily-fullstack-dev*
*Markdown mirror: https://skills.yangsir.net/api/skill/daily-fullstack-dev/markdown*