- Replace placeholder project_scaffolder.py with real implementation supporting Next.js, FastAPI+React, MERN, Django+React templates - Replace placeholder code_quality_analyzer.py with real implementation for security, complexity, dependencies, and test coverage analysis - Delete redundant fullstack_scaffolder.py (functionality in project_scaffolder) - Rewrite architecture_patterns.md with real patterns: frontend architecture, backend patterns, API design, caching, auth - Rewrite development_workflows.md with real workflows: Docker setup, git workflows, CI/CD, testing, deployment strategies - Rewrite tech_stack_guide.md with real comparisons: framework selection, database choices, auth solutions, deployment - Rewrite SKILL.md with TOC, trigger phrases, actual tool parameters Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
785 lines
16 KiB
Markdown
785 lines
16 KiB
Markdown
# Fullstack Development Workflows
|
|
|
|
Complete development lifecycle workflows from local setup to production deployment.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
- [Local Development Setup](#local-development-setup)
|
|
- [Git Workflows](#git-workflows)
|
|
- [CI/CD Pipelines](#cicd-pipelines)
|
|
- [Testing Strategies](#testing-strategies)
|
|
- [Code Review Process](#code-review-process)
|
|
- [Deployment Strategies](#deployment-strategies)
|
|
- [Monitoring and Observability](#monitoring-and-observability)
|
|
|
|
---
|
|
|
|
## Local Development Setup
|
|
|
|
### Docker Compose Development Environment
|
|
|
|
```yaml
|
|
# 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:**
|
|
|
|
```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
|
|
|
|
```bash
|
|
# .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:**
|
|
|
|
```typescript
|
|
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:**
|
|
|
|
```bash
|
|
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
|
|
|
|
```json
|
|
// package.json
|
|
{
|
|
"scripts": {
|
|
"prepare": "husky install"
|
|
},
|
|
"lint-staged": {
|
|
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
|
|
"*.{json,md}": ["prettier --write"]
|
|
}
|
|
}
|
|
```
|
|
|
|
```bash
|
|
# .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
|
|
|
|
```yaml
|
|
# .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
|
|
|
|
```yaml
|
|
# 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```markdown
|
|
## 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
```
|