Fullstack Project Scaffolding and Code-Quality Audits
| Field | Value |
|---|---|
| Type | Skill Resource |
| Source | ~/.copilot/skills/architecture/references/fullstack-scaffolding.md |
| Description | Not specified |
Source Content
Fullstack Project Scaffolding and Code-Quality Audits
Reference material for end-to-end web app scaffolding (Next.js, FastAPI+React, MERN, Django+React, Astro, Fiber) and for auditing an existing codebase’s security, complexity, dependency health, coverage, and docs into a P0/P1/P2 fix list. This material is scoped to scaffolding and audit scoring — component-level frontend linting lives in the frontend skill, not here.
Stack decision matrix
| Requirement | Recommendation |
|---|---|
| SEO-critical content site | Astro (preferred) or Next.js SSR |
| Internal dashboard | React + Vite + TanStack Router |
| API-first backend (Go shop) | Fiber via golang-fiber-bootstrapper |
| API-first backend (Python shop) | FastAPI |
| Real-time features | WebSocket layer + Postgres LISTEN/NOTIFY or NATS |
| Document-heavy data | Postgres jsonb first; MongoDB only if measured |
| Complex relational queries | Postgres + sqlc/Kysely |
Scaffolding workflow
- Clarify — audience, scale (10 users or 10M), team skill, deploy target, time horizon.
- Pick the stack via the matrix above, not by what’s trending this week.
- Draw boundaries — front-end / API / data / async — before writing any code.
- Scaffold with
scripts/project_scaffolder.py— emits a runnable project (TypeScript strict, Zod at boundaries, Docker, env example, CI skeleton). - Audit existing code when asked, with
scripts/code_quality_analyzer.py— scores security / complexity / deps / coverage / docs and ranks fixes P0/P1/P2. - Hand off — name which deeper skill picks up which boundary going forward (
api-designerfor the contract,database-designerfor schema,frontendfor component-level React work).
Part 1 — Fullstack Architecture Patterns (frontend/backend/API/DB/cache/auth)
Proven architectural patterns for scalable fullstack applications covering frontend, backend, and their integration.
Table of Contents
- Frontend Architecture
- Backend Architecture
- API Design Patterns
- Database Patterns
- Caching Strategies
- Authentication Architecture
Frontend Architecture
Component Architecture
Atomic Design Pattern
Organize components in hierarchical levels:
src/components/├── atoms/ # Button, Input, Icon├── molecules/ # SearchInput, FormField├── organisms/ # Header, Footer, Sidebar├── templates/ # PageLayout, DashboardLayout└── pages/ # Home, Profile, SettingsWhen to use: Large applications with design systems and multiple teams.
Container/Presentational Pattern
// Presentational - pure rendering, no statefunction UserCard({ name, email, avatar }: UserCardProps) { return ( <div className="card"> <img src={avatar} alt={name} /> <h3>{name}</h3> <p>{email}</p> </div> );}
// Container - handles data fetching and statefunction UserCardContainer({ userId }: { userId: string }) { const { data, loading } = useUser(userId); if (loading) return <Skeleton />; return <UserCard {...data} />;}When to use: When you need clear separation between UI and logic.
State Management Patterns
Server State vs Client State
| Type | Examples | Tools |
|---|---|---|
| Server State | User data, API responses | React Query, SWR |
| Client State | UI toggles, form inputs | Zustand, Jotai |
| URL State | Filters, pagination | Next.js router |
React Query for Server State:
function useUsers(filters: Filters) { return useQuery({ queryKey: ["users", filters], queryFn: () => api.getUsers(filters), staleTime: 5 * 60 * 1000, // 5 minutes gcTime: 30 * 60 * 1000, // 30 minutes });}
// Mutations with optimistic updatesfunction useUpdateUser() { const queryClient = useQueryClient();
return useMutation({ mutationFn: api.updateUser, onMutate: async (newUser) => { await queryClient.cancelQueries({ queryKey: ["users"] }); const previous = queryClient.getQueryData(["users"]); queryClient.setQueryData(["users"], (old) => old.map(u => u.id === newUser.id ? newUser : u) ); return { previous }; }, onError: (err, newUser, context) => { queryClient.setQueryData(["users"], context.previous); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ["users"] }); }, });}Backend Architecture
Clean Architecture
src/├── domain/ # Business entities, no dependencies│ ├── entities/ # User, Order, Product│ └── interfaces/ # Repository interfaces├── application/ # Use cases, application logic│ ├── use-cases/ # CreateOrder, UpdateUser│ └── services/ # OrderService, AuthService├── infrastructure/ # External concerns│ ├── database/ # Repository implementations│ ├── http/ # Controllers, middleware│ └── external/ # Third-party integrations└── shared/ # Cross-cutting concerns ├── errors/ └── utils/Dependency Flow: domain ← application ← infrastructure
Repository Pattern:
// Domain interfaceinterface UserRepository { findById(id: string): Promise<User | null>; findByEmail(email: string): Promise<User | null>; save(user: User): Promise<User>; delete(id: string): Promise<void>;}
// Infrastructure implementationclass PostgresUserRepository implements UserRepository { constructor(private db: Database) {}
async findById(id: string): Promise<User | null> { const row = await this.db.query( "SELECT * FROM users WHERE id = $1", [id] ); return row ? this.toEntity(row) : null; }
private toEntity(row: UserRow): User { return new User({ id: row.id, email: row.email, name: row.name, createdAt: row.created_at, }); }}Middleware Pipeline
// Express middleware chainapp.use(cors());app.use(helmet());app.use(requestId());app.use(logger());app.use(authenticate());app.use(rateLimit());app.use("/api", routes);app.use(errorHandler());
// Custom middleware examplefunction requestId() { return (req: Request, res: Response, next: NextFunction) => { req.id = req.headers["x-request-id"] || crypto.randomUUID(); res.setHeader("x-request-id", req.id); next(); };}
function errorHandler() { return (err: Error, req: Request, res: Response, next: NextFunction) => { const status = err instanceof AppError ? err.status : 500; const message = status === 500 ? "Internal Server Error" : err.message;
logger.error({ err, requestId: req.id }); res.status(status).json({ error: message, requestId: req.id }); };}API Design Patterns
REST Best Practices
Resource Naming:
- Use nouns, not verbs:
/usersnot/getUsers - Use plural:
/usersnot/user - Nest for relationships:
/users/{id}/orders
HTTP Methods:
| Method | Purpose | Idempotent |
|---|---|---|
| GET | Retrieve | Yes |
| POST | Create | No |
| PUT | Replace | Yes |
| PATCH | Partial update | No |
| DELETE | Remove | Yes |
Response Envelope:
// Success response{ "data": { /* resource */ }, "meta": { "requestId": "abc-123", "timestamp": "2024-01-15T10:30:00Z" }}
// Paginated response{ "data": [/* items */], "pagination": { "page": 1, "pageSize": 20, "total": 150, "totalPages": 8 }}
// Error response{ "error": { "code": "VALIDATION_ERROR", "message": "Invalid input", "details": [ { "field": "email", "message": "Invalid email format" } ] }, "meta": { "requestId": "abc-123" }}GraphQL Architecture
Schema-First Design:
type Query { user(id: ID!): User users(filter: UserFilter, page: PageInput): UserConnection!}
type Mutation { createUser(input: CreateUserInput!): UserPayload! updateUser(id: ID!, input: UpdateUserInput!): UserPayload!}
type User { id: ID! email: String! profile: Profile orders(first: Int, after: String): OrderConnection!}
type UserPayload { user: User errors: [Error!]}Resolver Pattern:
const resolvers = { Query: { user: async (_, { id }, { dataSources }) => { return dataSources.userAPI.findById(id); }, }, User: { // Field resolver for related data orders: async (user, { first, after }, { dataSources }) => { return dataSources.orderAPI.findByUserId(user.id, { first, after }); }, },};DataLoader for N+1 Prevention:
const userLoader = new DataLoader(async (userIds: string[]) => { const users = await db.query( "SELECT * FROM users WHERE id = ANY($1)", [userIds] ); // Return in same order as input return userIds.map(id => users.find(u => u.id === id));});Database Patterns
Connection Pooling
// PostgreSQL with connection poolconst pool = new Pool({ host: process.env.DB_HOST, database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, max: 20, // Maximum connections idleTimeoutMillis: 30000, // Close idle connections connectionTimeoutMillis: 2000,});
// Prisma with connection poolconst prisma = new PrismaClient({ datasources: { db: { url: `${process.env.DATABASE_URL}?connection_limit=20&pool_timeout=10`, }, },});Transaction Patterns
// Unit of Work patternasync function transferFunds(from: string, to: string, amount: number) { return await prisma.$transaction(async (tx) => { const sender = await tx.account.update({ where: { id: from }, data: { balance: { decrement: amount } }, });
if (sender.balance < 0) { throw new InsufficientFundsError(); }
await tx.account.update({ where: { id: to }, data: { balance: { increment: amount } }, });
return tx.transaction.create({ data: { fromId: from, toId: to, amount }, }); });}Read Replicas
// Route reads to replicaconst readDB = new PrismaClient({ datasources: { db: { url: process.env.READ_DATABASE_URL } },});
const writeDB = new PrismaClient({ datasources: { db: { url: process.env.WRITE_DATABASE_URL } },});
class UserRepository { async findById(id: string) { return readDB.user.findUnique({ where: { id } }); }
async create(data: CreateUserData) { return writeDB.user.create({ data }); }}Caching Strategies
Cache Layers
Request → CDN Cache → Application Cache → Database Cache → DatabaseCache-Aside Pattern:
async function getUser(id: string): Promise<User> { const cacheKey = `user:${id}`;
// 1. Try cache const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); }
// 2. Fetch from database const user = await db.user.findUnique({ where: { id } }); if (!user) throw new NotFoundError();
// 3. Store in cache await redis.set(cacheKey, JSON.stringify(user), "EX", 3600);
return user;}
// Invalidate on updateasync function updateUser(id: string, data: UpdateData): Promise<User> { const user = await db.user.update({ where: { id }, data }); await redis.del(`user:${id}`); return user;}HTTP Cache Headers:
// Immutable assets (hashed filenames)res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
// API responsesres.setHeader("Cache-Control", "private, max-age=0, must-revalidate");res.setHeader("ETag", generateETag(data));
// Static pagesres.setHeader("Cache-Control", "public, max-age=3600, stale-while-revalidate=86400");Authentication Architecture
JWT + Refresh Token Flow
1. User logs in → Server returns access token (15min) + refresh token (7d)2. Client stores tokens (httpOnly cookie for refresh, memory for access)3. Access token expires → Client uses refresh token to get new pair4. Refresh token expires → User must log in againImplementation:
// Token generationfunction generateTokens(user: User) { const accessToken = jwt.sign( { sub: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: "15m" } );
const refreshToken = jwt.sign( { sub: user.id, tokenVersion: user.tokenVersion }, process.env.REFRESH_SECRET, { expiresIn: "7d" } );
return { accessToken, refreshToken };}
// Refresh endpointapp.post("/auth/refresh", async (req, res) => { const refreshToken = req.cookies.refreshToken;
try { const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET); const user = await db.user.findUnique({ where: { id: payload.sub } });
// Check token version (invalidation mechanism) if (user.tokenVersion !== payload.tokenVersion) { throw new Error("Token revoked"); }
const tokens = generateTokens(user); setRefreshCookie(res, tokens.refreshToken); res.json({ accessToken: tokens.accessToken }); } catch { res.status(401).json({ error: "Invalid refresh token" }); }});Session-Based Auth
// Redis session storeapp.use(session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === "production", httpOnly: true, sameSite: "lax", maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days },}));
// Loginapp.post("/auth/login", async (req, res) => { const user = await authenticate(req.body.email, req.body.password); req.session.userId = user.id; res.json({ user });});
// Middlewarefunction requireAuth(req, res, next) { if (!req.session.userId) { return res.status(401).json({ error: "Authentication required" }); } next();}Decision Matrix
| Pattern | Complexity | Scalability | When to Use |
|---|---|---|---|
| Monolith | Low | Medium | MVPs, small teams |
| Modular Monolith | Medium | High | Growing teams |
| Microservices | High | Very High | Large orgs, diverse tech |
| REST | Low | High | CRUD APIs, public APIs |
| GraphQL | Medium | High | Complex data needs, mobile apps |
| JWT Auth | Low | High | Stateless APIs, microservices |
| Session Auth | Low | Medium | Traditional web apps |
Part 2 — Fullstack Development Workflows (local dev, git, CI/CD, testing, deploy, observability)
Complete development lifecycle workflows from local setup to production deployment.
Table of Contents
- Local Development Setup
- Git Workflows
- CI/CD Pipelines
- Testing Strategies
- Code Review Process
- Deployment Strategies
- Monitoring and Observability
Local Development Setup
Docker Compose Development Environment
version: "3.8"
services: app: build: context: . target: development volumes: - .:/app - /app/node_modules ports: - "3000:3000" environment: - DATABASE_URL=postgresql://user:pass@db:5432/app - REDIS_URL=redis://redis:6379 depends_on: - db - redis
db: image: postgres:16-alpine environment: POSTGRES_USER: user POSTGRES_PASSWORD: pass POSTGRES_DB: app volumes: - postgres_data:/var/lib/postgresql/data ports: - "5432:5432"
redis: image: redis:7-alpine ports: - "6379:6379"
volumes: postgres_data:Multistage Dockerfile:
# Base stageFROM node:20-alpine AS baseWORKDIR /appRUN apk add --no-cache libc6-compat
# Development stageFROM base AS developmentCOPY package*.json ./RUN npm ciCOPY . .CMD ["npm", "run", "dev"]
# Builder stageFROM base AS builderCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build
# Production stageFROM base AS productionENV NODE_ENV=productionCOPY --from=builder /app/package*.json ./RUN npm ci --only=productionCOPY --from=builder /app/dist ./distUSER nodeCMD ["node", "dist/index.js"]Environment Configuration
# .env.local (development)DATABASE_URL="postgresql://user:pass@localhost:5432/app_dev"REDIS_URL="redis://localhost:6379"JWT_SECRET="development-secret-change-in-prod"LOG_LEVEL="debug"
# .env.testDATABASE_URL="postgresql://user:pass@localhost:5432/app_test"LOG_LEVEL="error"
# .env.production (via secrets management)DATABASE_URL="${DATABASE_URL}"REDIS_URL="${REDIS_URL}"JWT_SECRET="${JWT_SECRET}"Environment validation:
import { z } from "zod";
const envSchema = z.object({ NODE_ENV: z.enum(["development", "test", "production"]), DATABASE_URL: z.string().url(), REDIS_URL: z.string().url().optional(), JWT_SECRET: z.string().min(32), PORT: z.coerce.number().default(3000),});
export const env = envSchema.parse(process.env);Git Workflows
Trunk-Based Development
main (protected) │ ├── feature/user-auth (short-lived, 1-2 days max) │ └── squash merge → main │ ├── feature/payment-flow │ └── squash merge → main │ └── release/v1.2.0 (cut from main for hotfixes)Branch naming:
feature/description- New featuresfix/description- Bug fixeschore/description- Maintenance tasksrelease/vX.Y.Z- Release branches
Commit Standards
Conventional Commits:
<type>(<scope>): <description>
[optional body]
[optional footer(s)]Types:
feat: New featurefix: Bug fixdocs: Documentationstyle: Formattingrefactor: Code restructuringtest: Adding testschore: Maintenance
Examples:
feat(auth): add password reset flow
Implement password reset with email verification.Tokens expire after 1 hour.
Closes #123
---
fix(api): handle null response in user endpoint
The API was returning 500 when user profile was incomplete.Now returns partial data with null fields.
---
chore(deps): update Next.js to 14.1.0
Breaking changes addressed:- Updated Image component usage- Migrated to new metadata APIPre-commit Hooks
{ "scripts": { "prepare": "husky install" }, "lint-staged": { "*.{ts,tsx}": ["eslint --fix", "prettier --write"], "*.{json,md}": ["prettier --write"] }}#!/bin/sh. "$(dirname "$0")/_/husky.sh"
npx lint-staged
# .husky/commit-msg#!/bin/sh. "$(dirname "$0")/_/husky.sh"
npx commitlint --edit $1CI/CD Pipelines
GitHub Actions
name: CI
on: push: branches: [main] pull_request: branches: [main]
jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm - run: npm ci - run: npm run lint - run: npm run type-check
test: runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_USER: test POSTGRES_PASSWORD: test POSTGRES_DB: test ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm - run: npm ci - run: npm run test:unit - run: npm run test:integration env: DATABASE_URL: postgresql://test:test@localhost:5432/test - uses: codecov/codecov-action@v3
build: runs-on: ubuntu-latest needs: [lint, test] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm - run: npm ci - run: npm run build - uses: actions/upload-artifact@v4 with: name: build path: dist/
deploy-preview: if: github.event_name == 'pull_request' runs-on: ubuntu-latest needs: build steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: name: build path: dist/ # Deploy to preview environment - name: Deploy Preview run: | # Deploy logic here echo "Deployed to preview-${{ github.event.pull_request.number }}"
deploy-production: if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest needs: build environment: production steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: name: build path: dist/ - name: Deploy Production run: | # Production deployment echo "Deployed to production"Database Migrations in CI
# Part of deploy job- name: Run Migrations run: | npx prisma migrate deploy env: DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Verify Migration run: | npx prisma migrate statusTesting Strategies
Testing Pyramid
/\ / \ E2E Tests (10%) / \ - Critical user journeys /──────\ / \ Integration Tests (20%) / \ - API endpoints /────────────\ - Database operations / \ / \ Unit Tests (70%)/──────────────────\ - Components, hooks, utilitiesUnit Testing
// Component test with React Testing Libraryimport { render, screen, fireEvent } from "@testing-library/react";import { UserForm } from "./UserForm";
describe("UserForm", () => { it("submits form with valid data", async () => { const onSubmit = vi.fn(); render(<UserForm onSubmit={onSubmit} />);
fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "test@example.com" }, }); fireEvent.change(screen.getByLabelText(/name/i), { target: { value: "John Doe" }, }); fireEvent.click(screen.getByRole("button", { name: /submit/i }));
await waitFor(() => { expect(onSubmit).toHaveBeenCalledWith({ email: "test@example.com", name: "John Doe", }); }); });
it("shows validation error for invalid email", async () => { render(<UserForm onSubmit={vi.fn()} />);
fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "invalid" }, }); fireEvent.click(screen.getByRole("button", { name: /submit/i }));
expect(await screen.findByText(/invalid email/i)).toBeInTheDocument(); });});Integration Testing
// API integration testimport { createTestClient } from "./test-utils";import { db } from "@/lib/db";
describe("POST /api/users", () => { beforeEach(async () => { await db.user.deleteMany(); });
it("creates user with valid data", async () => { const client = createTestClient();
const response = await client.post("/api/users", { email: "new@example.com", name: "New User", });
expect(response.status).toBe(201); expect(response.data.user.email).toBe("new@example.com");
// Verify in database const user = await db.user.findUnique({ where: { email: "new@example.com" }, }); expect(user).toBeTruthy(); });
it("returns 409 for duplicate email", async () => { await db.user.create({ data: { email: "existing@example.com", name: "Existing" }, });
const client = createTestClient();
const response = await client.post("/api/users", { email: "existing@example.com", name: "Duplicate", });
expect(response.status).toBe(409); expect(response.data.error.code).toBe("EMAIL_EXISTS"); });});E2E Testing with Playwright
import { test, expect } from "@playwright/test";
test.describe("Authentication", () => { test("user can log in and access dashboard", async ({ page }) => { await page.goto("/login");
await page.fill('[name="email"]', "user@example.com"); await page.fill('[name="password"]', "password123"); await page.click('button[type="submit"]');
await expect(page).toHaveURL("/dashboard"); await expect(page.locator("h1")).toHaveText("Welcome back"); });
test("shows error for invalid credentials", async ({ page }) => { await page.goto("/login");
await page.fill('[name="email"]', "wrong@example.com"); await page.fill('[name="password"]', "wrongpassword"); await page.click('button[type="submit"]');
await expect(page.locator('[role="alert"]')).toHaveText( "Invalid email or password" ); });});Code Review Process
PR Template
## Summary<!-- Brief description of changes -->
## Type of Change- [ ] Bug fix- [ ] New feature- [ ] Breaking change- [ ] Documentation update
## Changes Made<!-- List specific changes -->
## Testing- [ ] Unit tests added/updated- [ ] Integration tests added/updated- [ ] Manual testing completed
## Screenshots<!-- If applicable -->
## Checklist- [ ] Code follows style guidelines- [ ] Self-review completed- [ ] Documentation updated- [ ] No new warningsReview Checklist
Functionality:
- Does the code do what it’s supposed to?
- Are edge cases handled?
- Is error handling appropriate?
Code Quality:
- Is the code readable and maintainable?
- Are there any code smells?
- Is there unnecessary duplication?
Performance:
- Are there N+1 queries?
- Is caching used appropriately?
- Are there memory leaks?
Security:
- Is user input validated?
- Are there injection vulnerabilities?
- Is sensitive data protected?
Deployment Strategies
Blue-Green Deployment
Load Balancer │ ┌────────────┴────────────┐ │ │ ┌────┴────┐ ┌─────┴────┐ │ Blue │ │ Green │ │ (Live) │ │ (Idle) │ └─────────┘ └──────────┘
1. Deploy new version to Green2. Run smoke tests on Green3. Switch traffic to Green4. Blue becomes idle (rollback target)Canary Deployment
Load Balancer │ ┌────────────┴────────────┐ │ │ │ 95% 5% │ ▼ ▼ ┌─────────┐ ┌──────────┐ │ Stable │ │ Canary │ │ v1.0.0 │ │ v1.1.0 │ └─────────┘ └──────────┘
1. Deploy canary with small traffic %2. Monitor error rates, latency3. Gradually increase traffic4. Full rollout or rollbackFeature Flags
// Feature flag serviceconst flags = { newCheckoutFlow: { enabled: true, rolloutPercentage: 25, allowedUsers: ["beta-testers"], },};
function isFeatureEnabled(flag: string, userId: string): boolean { const config = flags[flag]; if (!config?.enabled) return false;
// Check allowed users if (config.allowedUsers?.includes(userId)) return true;
// Check rollout percentage const hash = hashUserId(userId); return hash < config.rolloutPercentage;}
// Usageif (isFeatureEnabled("newCheckoutFlow", user.id)) { return <NewCheckout />;}return <LegacyCheckout />;Monitoring and Observability
Structured Logging
import pino from "pino";
const logger = pino({ level: process.env.LOG_LEVEL || "info", formatters: { level: (label) => ({ level: label }), },});
// Request logging middlewareapp.use((req, res, next) => { const start = Date.now(); const requestId = req.headers["x-request-id"] || crypto.randomUUID();
res.on("finish", () => { logger.info({ type: "request", requestId, method: req.method, path: req.path, statusCode: res.statusCode, duration: Date.now() - start, userAgent: req.headers["user-agent"], }); });
next();});
// Application logginglogger.info({ userId: user.id, action: "login" }, "User logged in");logger.error({ err, orderId }, "Failed to process order");Metrics Collection
import { Counter, Histogram } from "prom-client";
const httpRequestsTotal = new Counter({ name: "http_requests_total", help: "Total HTTP requests", labelNames: ["method", "path", "status"],});
const httpRequestDuration = new Histogram({ name: "http_request_duration_seconds", help: "HTTP request duration", labelNames: ["method", "path"], buckets: [0.1, 0.3, 0.5, 1, 3, 5, 10],});
// Middlewareapp.use((req, res, next) => { const end = httpRequestDuration.startTimer({ method: req.method, path: req.route?.path || req.path, });
res.on("finish", () => { httpRequestsTotal.inc({ method: req.method, path: req.route?.path || req.path, status: res.statusCode, }); end(); });
next();});Health Checks
app.get("/health", async (req, res) => { const checks = { database: await checkDatabase(), redis: await checkRedis(), memory: checkMemory(), };
const healthy = Object.values(checks).every((c) => c.status === "healthy");
res.status(healthy ? 200 : 503).json({ status: healthy ? "healthy" : "unhealthy", checks, timestamp: new Date().toISOString(), });});
async function checkDatabase() { try { await db.$queryRaw`SELECT 1`; return { status: "healthy" }; } catch (error) { return { status: "unhealthy", error: error.message }; }}
function checkMemory() { const used = process.memoryUsage(); const heapUsedMB = Math.round(used.heapUsed / 1024 / 1024); const heapTotalMB = Math.round(used.heapTotal / 1024 / 1024);
return { status: heapUsedMB < heapTotalMB * 0.9 ? "healthy" : "warning", heapUsedMB, heapTotalMB, };}Quick Reference
Daily Workflow
# 1. Start workgit checkout main && git pullgit checkout -b feature/my-feature
# 2. Develop with hot reloaddocker-compose up -dnpm run dev
# 3. Test changesnpm run testnpm run lint
# 4. Commitgit add -Agit commit -m "feat(scope): description"
# 5. Push and create PRgit push -u origin feature/my-featuregh pr createRelease Workflow
# 1. Ensure main is stablegit checkout mainnpm run test:all
# 2. Create releasenpm version minor # or major/patchgit push --follow-tags
# 3. Verify deployment# CI/CD deploys automatically# Monitor dashboardsPart 3 — Fullstack Tech Stack Guide (framework/DB/ORM/auth/deploy comparisons)
Technology selection guide with trade-offs, use cases, and integration patterns for modern fullstack development.
Table of Contents
- Frontend Frameworks
- Backend Frameworks
- Databases
- ORMs and Query Builders
- Authentication Solutions
- Deployment Platforms
- Stack Recommendations
Frontend Frameworks
Next.js
Best for: Production React apps, SEO-critical sites, full-stack applications
| Pros | Cons |
|---|---|
| Server components, streaming | Learning curve for advanced features |
| Built-in routing, API routes | Vercel lock-in concerns |
| Excellent DX and performance | Bundle size can grow |
| Strong TypeScript support | Complex mental model (client/server) |
When to choose:
- Need SSR/SSG for SEO
- Building a product that may scale
- Want full-stack in one framework
- Team familiar with React
// App Router patternasync function UsersPage() { const users = await db.user.findMany(); // Server component return <UserList users={users} />;}
// app/users/[id]/page.tsxexport async function generateStaticParams() { const users = await db.user.findMany(); return users.map((user) => ({ id: user.id }));}React + Vite
Best for: SPAs, dashboards, internal tools
| Pros | Cons |
|---|---|
| Fast development with HMR | No SSR out of the box |
| Simple mental model | Manual routing setup |
| Flexible architecture | No built-in API routes |
| Smaller bundle potential | Need separate backend |
When to choose:
- Building internal dashboards
- SEO not important
- Need maximum flexibility
- Prefer decoupled frontend/backend
Vue 3
Best for: Teams transitioning from jQuery, progressive enhancement
| Pros | Cons |
|---|---|
| Gentle learning curve | Smaller ecosystem than React |
| Excellent documentation | Fewer enterprise adoptions |
| Single-file components | Composition API learning curve |
| Good TypeScript support | Two paradigms (Options/Composition) |
When to choose:
- Team new to modern frameworks
- Progressive enhancement needed
- Prefer official solutions (Pinia, Vue Router)
Comparison Matrix
| Feature | Next.js | React+Vite | Vue 3 | Svelte |
|---|---|---|---|---|
| SSR | Built-in | Manual | Nuxt | SvelteKit |
| Bundle size | Medium | Small | Small | Smallest |
| Learning curve | Medium | Low | Low | Low |
| Enterprise adoption | High | High | Medium | Low |
| Job market | Large | Large | Medium | Small |
Backend Frameworks
Node.js Ecosystem
Express.js
import express from "express";import { userRouter } from "./routes/users";
const app = express();app.use(express.json());app.use("/api/users", userRouter);app.listen(3000);| Pros | Cons |
|---|---|
| Minimal, flexible | No structure opinions |
| Huge middleware ecosystem | Callback-based (legacy) |
| Well understood | Manual TypeScript setup |
Fastify
import Fastify from "fastify";
const app = Fastify({ logger: true });
app.get("/users/:id", { schema: { params: { type: "object", properties: { id: { type: "string" } } }, response: { 200: UserSchema }, }, handler: async (request) => { return db.user.findUnique({ where: { id: request.params.id } }); },});| Pros | Cons |
|---|---|
| High performance | Smaller ecosystem |
| Built-in validation | Different plugin model |
| TypeScript-first | Less community content |
NestJS
@Controller("users")export class UsersController { constructor(private usersService: UsersService) {}
@Get(":id") findOne(@Param("id") id: string) { return this.usersService.findOne(id); }
@Post() @UseGuards(AuthGuard) create(@Body() createUserDto: CreateUserDto) { return this.usersService.create(createUserDto); }}| Pros | Cons |
|---|---|
| Strong architecture | Steep learning curve |
| Full-featured (GraphQL, WebSockets) | Heavy for small projects |
| Enterprise-ready | Decorator complexity |
Python Ecosystem
FastAPI
from fastapi import FastAPI, Dependsfrom sqlalchemy.orm import Session
app = FastAPI()
@app.get("/users/{user_id}", response_model=UserResponse)async def get_user(user_id: int, db: Session = Depends(get_db)): user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException(status_code=404) return user| Pros | Cons |
|---|---|
| Auto-generated docs | Python GIL limitations |
| Type hints → validation | Async ecosystem maturing |
| High performance | Smaller than Django ecosystem |
Django
| Pros | Cons |
|---|---|
| Batteries included | Monolithic |
| Admin panel | ORM limitations |
| Mature ecosystem | Async support newer |
Framework Selection Guide
| Use Case | Recommendation |
|---|---|
| API-first startup | FastAPI or Fastify |
| Enterprise backend | NestJS or Django |
| Microservices | Fastify or Go |
| Rapid prototype | Express or Django |
| Full-stack TypeScript | Next.js API routes |
Databases
PostgreSQL
Best for: Most applications, relational data, ACID compliance
-- JSON supportCREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) UNIQUE NOT NULL, profile JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT NOW());
-- Full-text searchCREATE INDEX users_search_idx ON users USING GIN (to_tsvector('english', email || ' ' || profile->>'name'));
SELECT * FROM usersWHERE to_tsvector('english', email || ' ' || profile->>'name') @@ to_tsquery('john');| Feature | Rating |
|---|---|
| ACID compliance | Excellent |
| JSON support | Excellent |
| Full-text search | Good |
| Horizontal scaling | Requires setup |
| Managed options | Many (RDS, Supabase, Neon) |
MongoDB
Best for: Document-heavy apps, flexible schemas, rapid prototyping
// Flexible schemaconst userSchema = new Schema({ email: { type: String, required: true, unique: true }, profile: { name: String, preferences: Schema.Types.Mixed, // Any structure }, orders: [{ type: Schema.Types.ObjectId, ref: "Order" }],});| Feature | Rating |
|---|---|
| Schema flexibility | Excellent |
| Horizontal scaling | Excellent |
| Transactions | Good (4.0+) |
| Joins | Limited |
| Managed options | Atlas |
Redis
Best for: Caching, sessions, real-time features, queues
// Session storageawait redis.set(`session:${sessionId}`, JSON.stringify(user), "EX", 3600);
// Rate limitingconst requests = await redis.incr(`rate:${ip}`);if (requests === 1) await redis.expire(`rate:${ip}`, 60);if (requests > 100) throw new TooManyRequestsError();
// Pub/Subredis.publish("notifications", JSON.stringify({ userId, message }));Database Selection Matrix
| Requirement | PostgreSQL | MongoDB | MySQL |
|---|---|---|---|
| Complex queries | Best | Limited | Good |
| Schema flexibility | Good (JSONB) | Best | Limited |
| Transactions | Best | Good | Good |
| Horizontal scale | Manual | Built-in | Manual |
| Cloud managed | Many | Atlas | Many |
ORMs and Query Builders
Prisma
Best for: TypeScript projects, schema-first development
// schema.prismamodel User { id String @id @default(cuid()) email String @unique posts Post[] profile Profile? createdAt DateTime @default(now())}
// Usage - fully typedconst user = await prisma.user.findUnique({ where: { email: "user@example.com" }, include: { posts: true, profile: true },});// user.posts is Post[] - TypeScript knows| Pros | Cons |
|---|---|
| Excellent TypeScript | Generated client size |
| Schema migrations | Limited raw SQL support |
| Visual studio | Some edge case limitations |
Drizzle
Best for: SQL-first TypeScript, performance-critical apps
// Schema definitionconst users = pgTable("users", { id: uuid("id").primaryKey().defaultRandom(), email: varchar("email", { length: 255 }).notNull().unique(), createdAt: timestamp("created_at").defaultNow(),});
// Query - SQL-like syntaxconst result = await db .select() .from(users) .where(eq(users.email, "user@example.com")) .leftJoin(posts, eq(posts.userId, users.id));| Pros | Cons |
|---|---|
| Lightweight | Newer, smaller community |
| SQL-like syntax | Fewer integrations |
| Fast runtime | Manual migrations |
SQLAlchemy (Python)
# Model definitionclass User(Base): __tablename__ = "users"
id = Column(UUID, primary_key=True, default=uuid4) email = Column(String(255), unique=True, nullable=False) posts = relationship("Post", back_populates="author")
# Queryusers = session.query(User)\ .filter(User.email.like("%@example.com"))\ .options(joinedload(User.posts))\ .all()Authentication Solutions
Auth.js (NextAuth)
Best for: Next.js apps, social logins
import NextAuth from "next-auth";import GitHub from "next-auth/providers/github";import Credentials from "next-auth/providers/credentials";
export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ GitHub, Credentials({ credentials: { email: {}, password: {} }, authorize: async (credentials) => { const user = await verifyCredentials(credentials); return user; }, }), ], callbacks: { jwt({ token, user }) { if (user) token.role = user.role; return token; }, },});| Pros | Cons |
|---|---|
| Many providers | Next.js focused |
| Session management | Complex customization |
| Database adapters | Breaking changes between versions |
Clerk
Best for: Rapid development, hosted solution
// Middlewareimport { clerkMiddleware } from "@clerk/nextjs/server";
export default clerkMiddleware();
// Usageimport { auth } from "@clerk/nextjs/server";
export async function GET() { const { userId } = await auth(); if (!userId) return new Response("Unauthorized", { status: 401 }); // ...}| Pros | Cons |
|---|---|
| Beautiful UI components | Vendor lock-in |
| Managed infrastructure | Cost at scale |
| Multi-factor auth | Data residency concerns |
Custom JWT
Best for: Full control, microservices
// Token generationfunction generateTokens(user: User) { const accessToken = jwt.sign( { sub: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: "15m" } );
const refreshToken = jwt.sign( { sub: user.id, version: user.tokenVersion }, process.env.REFRESH_SECRET, { expiresIn: "7d" } );
return { accessToken, refreshToken };}
// Middlewarefunction authenticate(req, res, next) { const token = req.headers.authorization?.replace("Bearer ", ""); if (!token) return res.status(401).json({ error: "No token" });
try { req.user = jwt.verify(token, process.env.JWT_SECRET); next(); } catch { res.status(401).json({ error: "Invalid token" }); }}Deployment Platforms
Vercel
Best for: Next.js, frontend-focused teams
| Pros | Cons |
|---|---|
| Zero-config Next.js | Expensive at scale |
| Edge functions | Vendor lock-in |
| Preview deployments | Limited backend options |
| Global CDN | Cold starts |
Railway
Best for: Full-stack apps, databases included
| Pros | Cons |
|---|---|
| Simple deployment | Smaller community |
| Built-in databases | Limited regions |
| Good pricing | Fewer integrations |
AWS (ECS/Lambda)
Best for: Enterprise, complex requirements
| Pros | Cons |
|---|---|
| Full control | Complex setup |
| Cost-effective at scale | Steep learning curve |
| Any technology | Requires DevOps knowledge |
Deployment Selection
| Requirement | Platform |
|---|---|
| Next.js simplicity | Vercel |
| Full-stack + DB | Railway, Render |
| Enterprise scale | AWS, GCP |
| Container control | Fly.io, Railway |
| Budget startup | Railway, Render |
Stack Recommendations
Startup MVP
Frontend: Next.js 14 (App Router)Backend: Next.js API RoutesDatabase: PostgreSQL (Neon/Supabase)Auth: Auth.js or ClerkDeploy: VercelCache: Vercel KV or Upstash RedisWhy: Fastest time to market, single deployment, good scaling path.
SaaS Product
Frontend: Next.js 14Backend: Separate API (FastAPI or NestJS)Database: PostgreSQL (RDS)Auth: Custom JWT + Auth.jsDeploy: Vercel (frontend) + AWS ECS (backend)Cache: Redis (ElastiCache)Queue: SQS or BullMQWhy: Separation allows independent scaling, team specialization.
Enterprise Application
Frontend: Next.js or React + ViteBackend: NestJS or GoDatabase: PostgreSQL (Aurora)Auth: Keycloak or Auth0Deploy: Kubernetes (EKS/GKE)Cache: Redis ClusterQueue: Kafka or RabbitMQObservability: Datadog or Grafana StackWhy: Maximum control, compliance requirements, team expertise.
Internal Tool
Frontend: React + Vite + TailwindBackend: Express or FastAPIDatabase: PostgreSQL or SQLiteAuth: OIDC with corporate IdPDeploy: Docker on internal infrastructureWhy: Simple, low maintenance, integrates with existing systems.
Quick Decision Guide
| Question | If Yes → | If No → |
|---|---|---|
| Need SEO? | Next.js SSR | React SPA |
| Complex backend? | Separate API | Next.js routes |
| Team knows Python? | FastAPI | Node.js |
| Need real-time? | Add WebSockets | REST is fine |
| Enterprise compliance? | Self-hosted | Managed services |
| Budget constrained? | Railway/Render | Vercel/AWS |
| Schema changes often? | MongoDB | PostgreSQL |