Skip to content

Fullstack Project Scaffolding and Code-Quality Audits

FieldValue
TypeSkill Resource
Source~/.copilot/skills/architecture/references/fullstack-scaffolding.md
DescriptionNot 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

RequirementRecommendation
SEO-critical content siteAstro (preferred) or Next.js SSR
Internal dashboardReact + Vite + TanStack Router
API-first backend (Go shop)Fiber via golang-fiber-bootstrapper
API-first backend (Python shop)FastAPI
Real-time featuresWebSocket layer + Postgres LISTEN/NOTIFY or NATS
Document-heavy dataPostgres jsonb first; MongoDB only if measured
Complex relational queriesPostgres + sqlc/Kysely

Scaffolding workflow

  1. Clarify — audience, scale (10 users or 10M), team skill, deploy target, time horizon.
  2. Pick the stack via the matrix above, not by what’s trending this week.
  3. Draw boundaries — front-end / API / data / async — before writing any code.
  4. Scaffold with scripts/project_scaffolder.py — emits a runnable project (TypeScript strict, Zod at boundaries, Docker, env example, CI skeleton).
  5. Audit existing code when asked, with scripts/code_quality_analyzer.py — scores security / complexity / deps / coverage / docs and ranks fixes P0/P1/P2.
  6. Hand off — name which deeper skill picks up which boundary going forward (api-designer for the contract, database-designer for schema, frontend for 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

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, Settings

When to use: Large applications with design systems and multiple teams.

Container/Presentational Pattern

// Presentational - pure rendering, no state
function 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 state
function 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

TypeExamplesTools
Server StateUser data, API responsesReact Query, SWR
Client StateUI toggles, form inputsZustand, Jotai
URL StateFilters, paginationNext.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 updates
function 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 interface
interface UserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
save(user: User): Promise<User>;
delete(id: string): Promise<void>;
}
// Infrastructure implementation
class 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 chain
app.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 example
function 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: /users not /getUsers
  • Use plural: /users not /user
  • Nest for relationships: /users/{id}/orders

HTTP Methods:

MethodPurposeIdempotent
GETRetrieveYes
POSTCreateNo
PUTReplaceYes
PATCHPartial updateNo
DELETERemoveYes

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 pool
const 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 pool
const prisma = new PrismaClient({
datasources: {
db: {
url: `${process.env.DATABASE_URL}?connection_limit=20&pool_timeout=10`,
},
},
});

Transaction Patterns

// Unit of Work pattern
async 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 replica
const 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 → Database

Cache-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 update
async 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 responses
res.setHeader("Cache-Control", "private, max-age=0, must-revalidate");
res.setHeader("ETag", generateETag(data));
// Static pages
res.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 pair
4. Refresh token expires → User must log in again

Implementation:

// Token generation
function 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 endpoint
app.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 store
app.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
},
}));
// Login
app.post("/auth/login", async (req, res) => {
const user = await authenticate(req.body.email, req.body.password);
req.session.userId = user.id;
res.json({ user });
});
// Middleware
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: "Authentication required" });
}
next();
}

Decision Matrix

PatternComplexityScalabilityWhen to Use
MonolithLowMediumMVPs, small teams
Modular MonolithMediumHighGrowing teams
MicroservicesHighVery HighLarge orgs, diverse tech
RESTLowHighCRUD APIs, public APIs
GraphQLMediumHighComplex data needs, mobile apps
JWT AuthLowHighStateless APIs, microservices
Session AuthLowMediumTraditional 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

Docker Compose Development Environment

docker-compose.yml
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 stage
FROM node:20-alpine AS base
WORKDIR /app
RUN apk add --no-cache libc6-compat
# Development stage
FROM base AS development
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npm", "run", "dev"]
# Builder stage
FROM base AS builder
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM base AS production
ENV NODE_ENV=production
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/index.js"]

Environment Configuration

Terminal window
# .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.test
DATABASE_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 features
  • fix/description - Bug fixes
  • chore/description - Maintenance tasks
  • release/vX.Y.Z - Release branches

Commit Standards

Conventional Commits:

<type>(<scope>): <description>
[optional body]
[optional footer(s)]

Types:

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation
  • style: Formatting
  • refactor: Code restructuring
  • test: Adding tests
  • chore: Maintenance

Examples:

Terminal window
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 API

Pre-commit Hooks

package.json
{
"scripts": {
"prepare": "husky install"
},
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md}": ["prettier --write"]
}
}
.husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
# .husky/commit-msg
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx commitlint --edit $1

CI/CD Pipelines

GitHub Actions

.github/workflows/ci.yml
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 status

Testing Strategies

Testing Pyramid

/\
/ \ E2E Tests (10%)
/ \ - Critical user journeys
/──────\
/ \ Integration Tests (20%)
/ \ - API endpoints
/────────────\ - Database operations
/ \
/ \ Unit Tests (70%)
/──────────────────\ - Components, hooks, utilities

Unit Testing

// Component test with React Testing Library
import { 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 test
import { 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

e2e/auth.spec.ts
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 warnings

Review 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 Green
2. Run smoke tests on Green
3. Switch traffic to Green
4. 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, latency
3. Gradually increase traffic
4. Full rollout or rollback

Feature Flags

// Feature flag service
const 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;
}
// Usage
if (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 middleware
app.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 logging
logger.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],
});
// Middleware
app.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

Terminal window
# 1. Start work
git checkout main && git pull
git checkout -b feature/my-feature
# 2. Develop with hot reload
docker-compose up -d
npm run dev
# 3. Test changes
npm run test
npm run lint
# 4. Commit
git add -A
git commit -m "feat(scope): description"
# 5. Push and create PR
git push -u origin feature/my-feature
gh pr create

Release Workflow

Terminal window
# 1. Ensure main is stable
git checkout main
npm run test:all
# 2. Create release
npm version minor # or major/patch
git push --follow-tags
# 3. Verify deployment
# CI/CD deploys automatically
# Monitor dashboards

Part 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

Next.js

Best for: Production React apps, SEO-critical sites, full-stack applications

ProsCons
Server components, streamingLearning curve for advanced features
Built-in routing, API routesVercel lock-in concerns
Excellent DX and performanceBundle size can grow
Strong TypeScript supportComplex 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/users/page.tsx
// App Router pattern
async function UsersPage() {
const users = await db.user.findMany(); // Server component
return <UserList users={users} />;
}
// app/users/[id]/page.tsx
export async function generateStaticParams() {
const users = await db.user.findMany();
return users.map((user) => ({ id: user.id }));
}

React + Vite

Best for: SPAs, dashboards, internal tools

ProsCons
Fast development with HMRNo SSR out of the box
Simple mental modelManual routing setup
Flexible architectureNo built-in API routes
Smaller bundle potentialNeed 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

ProsCons
Gentle learning curveSmaller ecosystem than React
Excellent documentationFewer enterprise adoptions
Single-file componentsComposition API learning curve
Good TypeScript supportTwo paradigms (Options/Composition)

When to choose:

  • Team new to modern frameworks
  • Progressive enhancement needed
  • Prefer official solutions (Pinia, Vue Router)

Comparison Matrix

FeatureNext.jsReact+ViteVue 3Svelte
SSRBuilt-inManualNuxtSvelteKit
Bundle sizeMediumSmallSmallSmallest
Learning curveMediumLowLowLow
Enterprise adoptionHighHighMediumLow
Job marketLargeLargeMediumSmall

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);
ProsCons
Minimal, flexibleNo structure opinions
Huge middleware ecosystemCallback-based (legacy)
Well understoodManual 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 } });
},
});
ProsCons
High performanceSmaller ecosystem
Built-in validationDifferent plugin model
TypeScript-firstLess 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);
}
}
ProsCons
Strong architectureSteep learning curve
Full-featured (GraphQL, WebSockets)Heavy for small projects
Enterprise-readyDecorator complexity

Python Ecosystem

FastAPI

from fastapi import FastAPI, Depends
from 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
ProsCons
Auto-generated docsPython GIL limitations
Type hints → validationAsync ecosystem maturing
High performanceSmaller than Django ecosystem

Django

ProsCons
Batteries includedMonolithic
Admin panelORM limitations
Mature ecosystemAsync support newer

Framework Selection Guide

Use CaseRecommendation
API-first startupFastAPI or Fastify
Enterprise backendNestJS or Django
MicroservicesFastify or Go
Rapid prototypeExpress or Django
Full-stack TypeScriptNext.js API routes

Databases

PostgreSQL

Best for: Most applications, relational data, ACID compliance

-- JSON support
CREATE 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 search
CREATE INDEX users_search_idx ON users
USING GIN (to_tsvector('english', email || ' ' || profile->>'name'));
SELECT * FROM users
WHERE to_tsvector('english', email || ' ' || profile->>'name')
@@ to_tsquery('john');
FeatureRating
ACID complianceExcellent
JSON supportExcellent
Full-text searchGood
Horizontal scalingRequires setup
Managed optionsMany (RDS, Supabase, Neon)

MongoDB

Best for: Document-heavy apps, flexible schemas, rapid prototyping

// Flexible schema
const 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" }],
});
FeatureRating
Schema flexibilityExcellent
Horizontal scalingExcellent
TransactionsGood (4.0+)
JoinsLimited
Managed optionsAtlas

Redis

Best for: Caching, sessions, real-time features, queues

// Session storage
await redis.set(`session:${sessionId}`, JSON.stringify(user), "EX", 3600);
// Rate limiting
const requests = await redis.incr(`rate:${ip}`);
if (requests === 1) await redis.expire(`rate:${ip}`, 60);
if (requests > 100) throw new TooManyRequestsError();
// Pub/Sub
redis.publish("notifications", JSON.stringify({ userId, message }));

Database Selection Matrix

RequirementPostgreSQLMongoDBMySQL
Complex queriesBestLimitedGood
Schema flexibilityGood (JSONB)BestLimited
TransactionsBestGoodGood
Horizontal scaleManualBuilt-inManual
Cloud managedManyAtlasMany

ORMs and Query Builders

Prisma

Best for: TypeScript projects, schema-first development

// schema.prisma
model User {
id String @id @default(cuid())
email String @unique
posts Post[]
profile Profile?
createdAt DateTime @default(now())
}
// Usage - fully typed
const user = await prisma.user.findUnique({
where: { email: "user@example.com" },
include: { posts: true, profile: true },
});
// user.posts is Post[] - TypeScript knows
ProsCons
Excellent TypeScriptGenerated client size
Schema migrationsLimited raw SQL support
Visual studioSome edge case limitations

Drizzle

Best for: SQL-first TypeScript, performance-critical apps

// Schema definition
const users = pgTable("users", {
id: uuid("id").primaryKey().defaultRandom(),
email: varchar("email", { length: 255 }).notNull().unique(),
createdAt: timestamp("created_at").defaultNow(),
});
// Query - SQL-like syntax
const result = await db
.select()
.from(users)
.where(eq(users.email, "user@example.com"))
.leftJoin(posts, eq(posts.userId, users.id));
ProsCons
LightweightNewer, smaller community
SQL-like syntaxFewer integrations
Fast runtimeManual migrations

SQLAlchemy (Python)

# Model definition
class 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")
# Query
users = 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

app/api/auth/[...nextauth]/route.ts
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;
},
},
});
ProsCons
Many providersNext.js focused
Session managementComplex customization
Database adaptersBreaking changes between versions

Clerk

Best for: Rapid development, hosted solution

// Middleware
import { clerkMiddleware } from "@clerk/nextjs/server";
export default clerkMiddleware();
// Usage
import { auth } from "@clerk/nextjs/server";
export async function GET() {
const { userId } = await auth();
if (!userId) return new Response("Unauthorized", { status: 401 });
// ...
}
ProsCons
Beautiful UI componentsVendor lock-in
Managed infrastructureCost at scale
Multi-factor authData residency concerns

Custom JWT

Best for: Full control, microservices

// Token generation
function 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 };
}
// Middleware
function 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

ProsCons
Zero-config Next.jsExpensive at scale
Edge functionsVendor lock-in
Preview deploymentsLimited backend options
Global CDNCold starts

Railway

Best for: Full-stack apps, databases included

ProsCons
Simple deploymentSmaller community
Built-in databasesLimited regions
Good pricingFewer integrations

AWS (ECS/Lambda)

Best for: Enterprise, complex requirements

ProsCons
Full controlComplex setup
Cost-effective at scaleSteep learning curve
Any technologyRequires DevOps knowledge

Deployment Selection

RequirementPlatform
Next.js simplicityVercel
Full-stack + DBRailway, Render
Enterprise scaleAWS, GCP
Container controlFly.io, Railway
Budget startupRailway, Render

Stack Recommendations

Startup MVP

Frontend: Next.js 14 (App Router)
Backend: Next.js API Routes
Database: PostgreSQL (Neon/Supabase)
Auth: Auth.js or Clerk
Deploy: Vercel
Cache: Vercel KV or Upstash Redis

Why: Fastest time to market, single deployment, good scaling path.

SaaS Product

Frontend: Next.js 14
Backend: Separate API (FastAPI or NestJS)
Database: PostgreSQL (RDS)
Auth: Custom JWT + Auth.js
Deploy: Vercel (frontend) + AWS ECS (backend)
Cache: Redis (ElastiCache)
Queue: SQS or BullMQ

Why: Separation allows independent scaling, team specialization.

Enterprise Application

Frontend: Next.js or React + Vite
Backend: NestJS or Go
Database: PostgreSQL (Aurora)
Auth: Keycloak or Auth0
Deploy: Kubernetes (EKS/GKE)
Cache: Redis Cluster
Queue: Kafka or RabbitMQ
Observability: Datadog or Grafana Stack

Why: Maximum control, compliance requirements, team expertise.

Internal Tool

Frontend: React + Vite + Tailwind
Backend: Express or FastAPI
Database: PostgreSQL or SQLite
Auth: OIDC with corporate IdP
Deploy: Docker on internal infrastructure

Why: Simple, low maintenance, integrates with existing systems.


Quick Decision Guide

QuestionIf Yes →If No →
Need SEO?Next.js SSRReact SPA
Complex backend?Separate APINext.js routes
Team knows Python?FastAPINode.js
Need real-time?Add WebSocketsREST is fine
Enterprise compliance?Self-hostedManaged services
Budget constrained?Railway/RenderVercel/AWS
Schema changes often?MongoDBPostgreSQL