feat: enhanced the writing skills

This commit is contained in:
duthaho
2026-04-18 18:50:39 +07:00
parent 7fa9a48c6c
commit 09538078e7
136 changed files with 10175 additions and 7947 deletions
+18 -17
View File
@@ -176,7 +176,7 @@ Modes adjust communication style, output format, and problem-solving approach.
|------|-------------|----------|
| `default` | Balanced standard behavior | General tasks |
| `brainstorm` | Creative exploration, questions | Design, ideation |
| `token-efficient` | Compressed, concise output | High-volume, cost savings |
| `writing-concisely` | Compressed, concise output | High-volume, cost savings |
| `deep-research` | Thorough analysis, citations | Investigation, audits |
| `implementation` | Code-focused, minimal prose | Executing plans |
| `review` | Critical analysis, finding issues | Code review, QA |
@@ -241,7 +241,7 @@ Control output verbosity for cost optimization.
/mode token-efficient # Enable for entire session
```
Reference: `.claude/skills/optimization/token-efficient/SKILL.md`
Reference: `.claude/skills/writing-concisely/SKILL.md`
## Context Management
@@ -328,31 +328,32 @@ To use subagent mode: `/execute-plan [plan-file]`
For strict TDD enforcement (no production code without failing test):
- Use `/tdd [feature]` command
- Reference: `.claude/skills/methodology/test-driven-development/SKILL.md`
- Reference: `.claude/skills/test-driven-development/SKILL.md`
### Verification Requirements
Enable mandatory verification before completion claims:
- Reference: `.claude/skills/methodology/verification-before-completion/SKILL.md`
- Reference: `.claude/skills/verification-before-completion/SKILL.md`
### Available Skills
| Category | Skills |
|----------|--------|
| **Languages** | python, typescript, javascript |
| **Frameworks** | fastapi, django, nextjs, react |
| **Databases** | postgresql, mongodb |
| **DevOps** | docker, github-actions |
| **Frontend** | tailwind, shadcn-ui |
| **Languages** | languages (Python, TypeScript, JavaScript) |
| **Backend** | backend-frameworks (FastAPI, Django, NestJS, Express) |
| **Frontend** | frontend (React, Next.js, shadcn/ui), frontend-styling (Tailwind, accessibility) |
| **Databases** | databases (PostgreSQL, MongoDB, Redis, migrations) |
| **DevOps** | devops (Docker, GitHub Actions, Cloudflare Workers) |
| **Security** | owasp |
| **API** | openapi |
| **Testing** | pytest, vitest |
| **Optimization** | token-efficient |
| **Developer Patterns** | error-handling, state-management, logging, caching, api-client, authentication |
| **Methodology - Planning** | brainstorming, writing-plans, executing-plans |
| **Testing** | testing (pytest, vitest, Jest), playwright |
| **Optimization** | writing-concisely |
| **Developer Patterns** | error-handling, state-management, logging, caching, api-client, authentication, background-jobs |
| **Methodology - Planning** | brainstorming, writing-plans, executing-plans, writing-skills |
| **Methodology - Testing** | test-driven-development, verification-before-completion, testing-anti-patterns |
| **Methodology - Debugging** | systematic-debugging, root-cause-tracing, defense-in-depth |
| **Methodology - Collaboration** | dispatching-parallel-agents, requesting-code-review, receiving-code-review, finishing-development-branch |
| **Methodology - Collaboration** | dispatching-parallel-agents, requesting-code-review, receiving-code-review, finishing-a-development-branch |
| **Methodology - Execution** | subagent-driven-development, using-git-worktrees, condition-based-waiting |
| **Methodology - Reasoning** | sequential-thinking |
Skills location: `.claude/skills/`
@@ -367,7 +368,7 @@ Each skill includes:
### Sequential Thinking
For complex problems requiring step-by-step analysis:
- Reference: `.claude/skills/methodology/sequential-thinking/SKILL.md`
- Reference: `.claude/skills/sequential-thinking/SKILL.md`
- Activation: `/research --sequential [topic]` or use deep-research mode
## Environment Configuration
@@ -455,9 +456,9 @@ pnpm install
## Kit Version
- **Claude Kit Version**: 3.0.0
- **Last Updated**: 2026-03-30
- **Last Updated**: 2026-04-18
- **Compatible with**: Claude Code 1.0+
- **Total Skills**: 38 (with YAML frontmatter, bundled resources)
- **Total Skills**: 36 (with YAML frontmatter, bundled resources)
- **Total Commands**: 27+
- **Total Agents**: 20
- **Behavioral Modes**: 7
+1 -1
View File
@@ -277,7 +277,7 @@ Starting from fundamentals, what's the best way to solve this?
For enhanced interactive brainstorming, use the superpowers methodology:
**Reference**: `.claude/skills/methodology/brainstorming/SKILL.md`
**Reference**: `.claude/skills/brainstorming/SKILL.md`
Key principles from superpowers methodology:
- **One question per message**: Ask single questions, wait for response
+3 -3
View File
@@ -183,7 +183,7 @@ For enhanced code review workflows, use the superpowers methodology:
### Requesting Reviews
**Reference**: `.claude/skills/methodology/requesting-code-review/SKILL.md`
**Reference**: `.claude/skills/requesting-code-review/SKILL.md`
Include in review requests:
- Scope definition (files, lines changed)
@@ -193,7 +193,7 @@ Include in review requests:
### Receiving Reviews
**Reference**: `.claude/skills/methodology/receiving-code-review/SKILL.md`
**Reference**: `.claude/skills/receiving-code-review/SKILL.md`
Process feedback by category:
- **Critical**: Must fix before proceeding
@@ -204,7 +204,7 @@ Process feedback by category:
When using subagent-driven development:
**Reference**: `.claude/skills/methodology/executing-plans/SKILL.md`
**Reference**: `.claude/skills/executing-plans/SKILL.md`
- Review after each task completion
- Fresh agent for unbiased review
+3 -3
View File
@@ -236,7 +236,7 @@ This agent works with:
For enhanced systematic debugging, use the superpowers methodology:
**Reference**: `.claude/skills/methodology/systematic-debugging/SKILL.md`
**Reference**: `.claude/skills/systematic-debugging/SKILL.md`
### Four-Phase Methodology
@@ -255,8 +255,8 @@ If 3+ consecutive fixes fail, STOP - this is an architectural problem.
### Additional Skills
- **Root cause tracing**: `.claude/skills/methodology/root-cause-tracing/SKILL.md`
- **Defense in depth**: `.claude/skills/methodology/defense-in-depth/SKILL.md`
- **Root cause tracing**: `.claude/skills/root-cause-tracing/SKILL.md`
- **Defense in depth**: `.claude/skills/defense-in-depth/SKILL.md`
<!-- CUSTOMIZATION POINT -->
## Project-Specific Overrides
+2 -2
View File
@@ -144,7 +144,7 @@ Implement JWT-based authentication with login, logout, and token refresh capabil
For enhanced detailed planning, use the superpowers methodology:
**Reference**: `.claude/skills/methodology/writing-plans/SKILL.md`
**Reference**: `.claude/skills/writing-plans/SKILL.md`
### Detailed Mode (2-5 min tasks)
@@ -161,7 +161,7 @@ After creating a detailed plan:
- **Subagent-driven**: Use `executing-plans` skill for automated execution
- **Manual**: Developer follows plan sequentially
**Reference**: `.claude/skills/methodology/executing-plans/SKILL.md`
**Reference**: `.claude/skills/executing-plans/SKILL.md`
<!-- CUSTOMIZATION POINT -->
## Project-Specific Overrides
+3 -3
View File
@@ -266,7 +266,7 @@ For enhanced testing practices, use the superpowers methodology:
### Test-Driven Development
**Reference**: `.claude/skills/methodology/test-driven-development/SKILL.md`
**Reference**: `.claude/skills/test-driven-development/SKILL.md`
Key principles:
- **NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST**
@@ -277,7 +277,7 @@ Key principles:
### Verification
**Reference**: `.claude/skills/methodology/verification-before-completion/SKILL.md`
**Reference**: `.claude/skills/verification-before-completion/SKILL.md`
Before claiming tests pass:
1. Identify the command that proves assertion
@@ -288,7 +288,7 @@ Before claiming tests pass:
### Testing Anti-Patterns
**Reference**: `.claude/skills/methodology/testing-anti-patterns/SKILL.md`
**Reference**: `.claude/skills/testing-anti-patterns/SKILL.md`
Avoid these mistakes:
1. Testing mock behavior instead of real code
+1 -1
View File
@@ -20,7 +20,7 @@ Start interactive brainstorming session for: **$ARGUMENTS**
## Methodology
**Reference**: `.claude/skills/methodology/brainstorming/SKILL.md`
**Reference**: `.claude/skills/brainstorming/SKILL.md`
This command uses the superpowers brainstorming methodology for optimal results.
+2 -2
View File
@@ -20,7 +20,7 @@ Execute plan from: **$ARGUMENTS**
## Methodology
**Reference**: `.claude/skills/methodology/executing-plans/SKILL.md`
**Reference**: `.claude/skills/executing-plans/SKILL.md`
This command uses the superpowers execution methodology for quality-gated implementation.
@@ -104,7 +104,7 @@ After all tasks complete:
2. Review entire implementation against plan
3. Verify all success criteria met
4. Run full test suite
5. Use `finishing-development-branch` skill
5. Use `finishing-a-development-branch` skill
## Critical Rules
+1 -1
View File
@@ -14,7 +14,7 @@ Switch to the specified behavioral mode.
|------|-------------|----------|
| `default` | Balanced standard behavior | General tasks |
| `brainstorm` | Creative exploration, more questions | Design, ideation |
| `token-efficient` | Compressed, concise output | High-volume, cost savings |
| `writing-concisely` | Compressed, concise output | High-volume, cost savings |
| `deep-research` | Thorough analysis, citations | Investigation, audits |
| `implementation` | Code-focused, minimal prose | Executing plans |
| `review` | Critical analysis, finding issues | Code review, QA |
+2 -2
View File
@@ -201,7 +201,7 @@ Use `--detailed` flag for superpowers-style plans with 2-5 minute tasks:
### Detailed Mode Features
**Reference**: `.claude/skills/methodology/writing-plans/SKILL.md`
**Reference**: `.claude/skills/writing-plans/SKILL.md`
When `--detailed` is specified:
- **Bite-sized tasks**: 2-5 minutes each (vs standard 15-60 min)
@@ -254,7 +254,7 @@ When `--detailed` is specified:
Use `/execute-plan [plan-file]` for subagent-driven execution with code review gates.
**Reference**: `.claude/skills/methodology/executing-plans/SKILL.md`
**Reference**: `.claude/skills/executing-plans/SKILL.md`
## Flags
+3 -3
View File
@@ -71,7 +71,7 @@ Add more test cases and repeat the cycle.
## Superpowers TDD Methodology
**Reference**: `.claude/skills/methodology/test-driven-development/SKILL.md`
**Reference**: `.claude/skills/test-driven-development/SKILL.md`
### Non-Negotiable Rule
@@ -90,7 +90,7 @@ RIGHT: Delete the code, write test, rewrite implementation
### Verification Before Completion
**Reference**: `.claude/skills/methodology/verification-before-completion/SKILL.md`
**Reference**: `.claude/skills/verification-before-completion/SKILL.md`
Before claiming tests pass:
1. **Identify** the command that proves assertion
@@ -108,7 +108,7 @@ Never use without verification:
### Testing Anti-Patterns to Avoid
**Reference**: `.claude/skills/methodology/testing-anti-patterns/SKILL.md`
**Reference**: `.claude/skills/testing-anti-patterns/SKILL.md`
1. Testing mock behavior instead of real code
2. Polluting production with test-only methods
+59
View File
@@ -0,0 +1,59 @@
---
name: api-client
description: >
Use when setting up axios, fetch, or httpx clients, implementing request interceptors, adding retry logic, handling authentication tokens, or generating type-safe API clients from OpenAPI specs. Also activate whenever code makes HTTP requests, integrates with external APIs, or needs robust error handling for network calls.
---
# API Client Patterns
## When to Use
- Setting up HTTP clients (httpx, axios, fetch) with base URLs and default headers
- Implementing request/response interceptors for auth tokens, logging, or error transformation
- Adding retry logic with exponential backoff for transient failures
- Generating type-safe API clients from OpenAPI specifications
- Handling authentication tokens (Bearer, API key) in outbound requests
- Building wrapper classes around third-party REST APIs
## When NOT to Use
- Building API servers (use `backend-frameworks`)
- Making simple one-off HTTP calls that don't need a configured client
- GraphQL clients (use Apollo or urql documentation directly)
---
## Quick Reference
| Topic | Reference | Key content |
|-------|-----------|-------------|
| All client patterns | `references/patterns.md` | httpx, axios, fetch wrappers, interceptors, retry, type-safe clients |
| HTTP client recipes | `references/http-client-patterns.md` | Advanced patterns, streaming, file uploads |
---
## Best Practices
1. **Create a single configured client instance.** Don't construct new clients per request. Configure base URL, timeouts, and default headers once.
2. **Set explicit timeouts.** Never use default (infinite) timeouts. Set connect and read timeouts separately.
3. **Use interceptors for cross-cutting concerns.** Auth token injection, request logging, and error transformation belong in interceptors, not in each call site.
4. **Implement retry with exponential backoff and jitter.** Only retry idempotent requests (GET, PUT, DELETE) and transient errors (5xx, network errors).
5. **Respect Retry-After headers.** When the server sends `Retry-After`, honor it instead of using your own backoff schedule.
6. **Generate clients from OpenAPI specs.** Use `openapi-typescript` + `openapi-fetch` (TypeScript) or `openapi-python-client` (Python) for type-safe API consumption.
7. **Handle errors at the boundary.** Transform HTTP errors into domain-specific errors at the client wrapper level.
## Common Pitfalls
1. **No timeout configured** — requests hang indefinitely on unresponsive servers.
2. **Retrying non-idempotent requests** — retrying POST can create duplicates. Use idempotency keys.
3. **Swallowing error details** — wrapping errors without preserving the original status code and message.
4. **Token refresh race conditions** — multiple concurrent requests all try to refresh the token simultaneously. Use a mutex/lock.
5. **Not closing client connections** — httpx AsyncClient and axios instances should be properly closed/disposed.
---
## Related Skills
- `error-handling` — Error transformation and retry patterns
- `authentication` — Token management for API calls
- `backend-frameworks` — Building the APIs these clients consume
@@ -1,8 +1,5 @@
---
name: api-client
description: >
HTTP client patterns for consuming REST APIs in Python and TypeScript. Use this skill when setting up axios, fetch, or httpx clients, implementing request interceptors, adding retry logic, handling authentication tokens, or generating type-safe API clients from OpenAPI specs. Trigger whenever code makes HTTP requests, integrates with external APIs, or needs robust error handling for network calls.
---
# Api Client — Patterns
# API Client Patterns
@@ -765,8 +762,8 @@ class ThrottledClient:
## Related Skills
- `api/openapi` - OpenAPI spec design and documentation
- `patterns/error-handling` - Structured error handling patterns across the stack
- `patterns/authentication` - Authentication token management and OAuth2 flows
- `patterns/caching` - HTTP caching, conditional requests, and cache invalidation
- `patterns/logging` - Logging HTTP requests and responses
- `openapi` - OpenAPI spec design and documentation
- `error-handling` - Structured error handling patterns across the stack
- `authentication` - Authentication token management and OAuth2 flows
- `caching` - HTTP caching, conditional requests, and cache invalidation
- `logging` - Logging HTTP requests and responses
-837
View File
@@ -1,837 +0,0 @@
---
name: openapi
description: >
Use this skill when designing, documenting, or generating REST API specifications using OpenAPI/Swagger. Trigger on keywords like OpenAPI, Swagger, API spec, REST documentation, API schema, request body, response schema, and API client generation. Also apply when adopting design-first API development, validating API contracts, or setting up auto-generated API documentation for FastAPI, Express, or NestJS endpoints.
---
# OpenAPI & REST API Design
## When to Use
- Documenting REST APIs
- Generating API clients
- API design-first development
- Defining webhook contracts
- Establishing pagination, versioning, or auth patterns for a new service
## When NOT to Use
- Internal-only scripts or automation that do not expose HTTP endpoints
- CLI tools and command-line utilities without a REST interface
- GraphQL APIs where a different specification format applies
---
## Core Patterns
### 1. OpenAPI 3.1 Specification Structure
A complete spec skeleton showing every top-level section. Use `$ref` to split
large specs into per-resource files.
```yaml
openapi: 3.1.0
info:
title: Acme API
version: 2.0.0
description: Public API for the Acme platform.
contact:
name: API Support
email: api@acme.dev
license:
name: MIT
url: https://opensource.org/licenses/MIT
servers:
- url: https://api.acme.dev/v2
description: Production
- url: https://staging-api.acme.dev/v2
description: Staging
tags:
- name: Users
description: User management operations
- name: Orders
description: Order lifecycle operations
paths:
/users:
$ref: './paths/users.yaml'
/users/{userId}:
$ref: './paths/users-by-id.yaml'
/orders:
$ref: './paths/orders.yaml'
components:
schemas:
$ref: './components/schemas/_index.yaml'
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
security:
- BearerAuth: []
webhooks:
orderCompleted:
$ref: './webhooks/order-completed.yaml'
```
**Organizing with `$ref`** -- keep one file per resource under `paths/` and
shared schemas under `components/schemas/`. A bundler such as
`@redocly/cli bundle` resolves references into a single file for tooling.
```
spec/
├── openapi.yaml # Root document
├── paths/
│ ├── users.yaml
│ ├── users-by-id.yaml
│ └── orders.yaml
├── components/
│ └── schemas/
│ ├── _index.yaml
│ ├── User.yaml
│ ├── Order.yaml
│ └── ProblemDetail.yaml
└── webhooks/
└── order-completed.yaml
```
---
### 2. Path & Operation Patterns
#### RESTful URL Naming Conventions
- Use **plural nouns** for collections: `/users`, `/orders`.
- Use **path parameters** for single-resource access: `/users/{userId}`.
- Nest only one level deep: `/users/{userId}/orders` (not deeper).
- Use **query parameters** for filtering, sorting, and pagination.
- Avoid verbs in paths -- let HTTP methods convey the action.
#### CRUD Operations
```yaml
paths:
/users:
get:
operationId: listUsers
tags: [Users]
summary: List users
parameters:
- $ref: '#/components/parameters/PageCursor'
- $ref: '#/components/parameters/PageSize'
- name: status
in: query
schema:
type: string
enum: [active, inactive]
responses:
'200':
description: Paginated list of users
content:
application/json:
schema:
$ref: '#/components/schemas/UserListResponse'
post:
operationId: createUser
tags: [Users]
summary: Create a user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: User created
headers:
Location:
schema:
type: string
description: URL of the new resource
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
/users/{userId}:
parameters:
- name: userId
in: path
required: true
schema:
type: string
format: uuid
get:
operationId: getUser
tags: [Users]
summary: Get a single user
responses:
'200':
description: User found
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
$ref: '#/components/responses/NotFound'
patch:
operationId: updateUser
tags: [Users]
summary: Partially update a user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateUserRequest'
responses:
'200':
description: User updated
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
$ref: '#/components/responses/NotFound'
'422':
$ref: '#/components/responses/ValidationError'
delete:
operationId: deleteUser
tags: [Users]
summary: Delete a user
responses:
'204':
description: User deleted
'404':
$ref: '#/components/responses/NotFound'
```
#### Path Parameters vs Query Parameters
| Use case | Mechanism | Example |
|----------|-----------|---------|
| Identify a specific resource | Path parameter | `/orders/{orderId}` |
| Filter a collection | Query parameter | `/orders?status=shipped` |
| Sort a collection | Query parameter | `/orders?sort=-createdAt` |
| Paginate | Query parameter | `/orders?cursor=abc&limit=20` |
| Expand nested data | Query parameter | `/orders?expand=items,customer` |
---
### 3. Request Body Patterns
#### JSON Request Body with Validation
```yaml
components:
schemas:
CreateUserRequest:
type: object
required:
- email
- name
properties:
email:
type: string
format: email
maxLength: 254
name:
type: string
minLength: 1
maxLength: 100
role:
type: string
enum: [admin, member, viewer]
default: member
additionalProperties: false
```
Implementation in **FastAPI** (Python):
```python
from pydantic import BaseModel, EmailStr, Field
class CreateUserRequest(BaseModel):
email: EmailStr
name: str = Field(min_length=1, max_length=100)
role: str = Field(default="member", pattern="^(admin|member|viewer)$")
model_config = {"extra": "forbid"}
```
Implementation in **Express** (TypeScript with Zod):
```typescript
import { z } from "zod";
const CreateUserRequest = z.object({
email: z.string().email().max(254),
name: z.string().min(1).max(100),
role: z.enum(["admin", "member", "viewer"]).default("member"),
}).strict();
type CreateUserRequest = z.infer<typeof CreateUserRequest>;
```
#### Multipart Form Data (File Uploads)
```yaml
/users/{userId}/avatar:
put:
operationId: uploadAvatar
tags: [Users]
summary: Upload user avatar
parameters:
- name: userId
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required:
- file
properties:
file:
type: string
format: binary
description: Image file (JPEG or PNG, max 5 MB)
caption:
type: string
maxLength: 200
encoding:
file:
contentType: image/jpeg, image/png
responses:
'200':
description: Avatar updated
content:
application/json:
schema:
type: object
properties:
url:
type: string
format: uri
'413':
$ref: '#/components/responses/PayloadTooLarge'
```
#### Content Negotiation
Support multiple response formats by listing them under `content`:
```yaml
responses:
'200':
description: Export data
content:
application/json:
schema:
$ref: '#/components/schemas/ExportData'
text/csv:
schema:
type: string
application/pdf:
schema:
type: string
format: binary
```
Clients select a format with the `Accept` header. Document which formats your
API actually supports so consumers do not have to guess.
---
### 4. Response Patterns
#### Success Responses
| Code | Meaning | Typical use |
|------|---------|-------------|
| `200` | OK | GET, PATCH, general success |
| `201` | Created | POST that creates a resource |
| `202` | Accepted | Async operation started |
| `204` | No Content | DELETE, or PUT with no body returned |
Always return a `Location` header with `201` pointing to the new resource.
#### Error Responses -- RFC 7807 Problem Details
Define a single reusable error schema based on RFC 7807:
```yaml
components:
schemas:
ProblemDetail:
type: object
required:
- type
- title
- status
properties:
type:
type: string
format: uri
description: URI reference identifying the problem type.
example: https://api.acme.dev/problems/validation-error
title:
type: string
description: Short human-readable summary.
example: Validation Error
status:
type: integer
description: HTTP status code.
example: 422
detail:
type: string
description: Human-readable explanation specific to this occurrence.
example: "Field 'email' must be a valid email address."
instance:
type: string
format: uri
description: URI identifying this specific occurrence.
errors:
type: array
description: Field-level validation errors (optional extension).
items:
type: object
properties:
field:
type: string
example: email
message:
type: string
example: Must be a valid email address.
code:
type: string
example: invalid_format
responses:
NotFound:
description: Resource not found
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
example:
type: https://api.acme.dev/problems/not-found
title: Not Found
status: 404
detail: User with ID '550e8400' was not found.
ValidationError:
description: Request validation failed
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
example:
type: https://api.acme.dev/problems/validation-error
title: Validation Error
status: 422
errors:
- field: email
message: Must be a valid email address.
code: invalid_format
Conflict:
description: Resource conflict
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
PayloadTooLarge:
description: Request payload exceeds limit
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
```
Use the `application/problem+json` media type for all error responses to signal
RFC 7807 compliance.
---
### 5. Authentication Schemes
#### Bearer Token (JWT)
```yaml
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- BearerAuth: []
```
Override per-operation to allow unauthenticated access:
```yaml
paths:
/health:
get:
security: [] # No auth required
responses:
'200':
description: Healthy
```
#### API Key
```yaml
components:
securitySchemes:
ApiKeyHeader:
type: apiKey
in: header
name: X-API-Key
ApiKeyQuery:
type: apiKey
in: query
name: api_key
```
#### OAuth2 Flows
```yaml
components:
securitySchemes:
OAuth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.acme.dev/authorize
tokenUrl: https://auth.acme.dev/token
refreshUrl: https://auth.acme.dev/token
scopes:
users:read: Read user profiles
users:write: Create and update users
orders:read: Read orders
paths:
/users:
get:
security:
- OAuth2: [users:read]
```
---
### 6. Pagination Patterns
#### Cursor-Based Pagination (Recommended)
Best for large, real-time datasets where rows may be inserted or deleted
between pages.
```yaml
components:
parameters:
PageCursor:
name: cursor
in: query
description: Opaque cursor returned by a previous response.
schema:
type: string
PageSize:
name: limit
in: query
description: Maximum items per page.
schema:
type: integer
minimum: 1
maximum: 100
default: 20
schemas:
UserListResponse:
type: object
required:
- data
- pagination
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
pagination:
type: object
required:
- hasMore
properties:
nextCursor:
type: string
nullable: true
hasMore:
type: boolean
```
#### Offset-Based Pagination
Simpler but less efficient for large tables and susceptible to drift when data
changes between requests.
```yaml
components:
parameters:
PageOffset:
name: offset
in: query
schema:
type: integer
minimum: 0
default: 0
PageLimit:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
schemas:
PaginatedResponse:
type: object
required:
- data
- total
- offset
- limit
properties:
data:
type: array
items: {}
total:
type: integer
description: Total number of matching records.
offset:
type: integer
limit:
type: integer
```
#### Response Envelope Pattern
Wrap every collection in a consistent envelope so clients always know where to
find the data and metadata:
```json
{
"data": [ ... ],
"pagination": { "nextCursor": "abc123", "hasMore": true },
"meta": { "requestId": "req_xyz", "timestamp": "2026-03-29T12:00:00Z" }
}
```
---
### 7. API Versioning
#### URL Versioning
```yaml
servers:
- url: https://api.acme.dev/v1
description: Version 1 (deprecated)
- url: https://api.acme.dev/v2
description: Version 2 (current)
```
Pros: explicit, easy to route, cache-friendly.
Cons: duplicates paths across versions, harder to share schemas.
#### Header Versioning
```yaml
parameters:
- name: X-API-Version
in: header
required: false
schema:
type: string
enum: ['2024-01-15', '2025-06-01']
default: '2025-06-01'
description: Date-based API version. Defaults to latest stable.
```
Pros: clean URLs, fine-grained control.
Cons: less discoverable, harder to test in a browser.
#### Trade-offs Summary
| Approach | Discoverability | URL cleanliness | Caching | Migration effort |
|----------|----------------|-----------------|---------|-----------------|
| URL path | High | Lower | Easy | Higher (path changes) |
| Header | Lower | High | Needs Vary header | Lower |
| Query param | Medium | Medium | Easy | Lower |
Pick one approach and use it consistently. URL versioning is the most common
choice for public APIs; header versioning suits internal services.
---
### 8. Webhook Specifications
OpenAPI 3.1 supports a top-level `webhooks` key for documenting outbound
event payloads your API will send to consumer-registered URLs.
```yaml
webhooks:
orderCompleted:
post:
operationId: onOrderCompleted
summary: Fired when an order reaches "completed" status.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OrderCompletedEvent'
responses:
'200':
description: Webhook received successfully.
components:
schemas:
WebhookEventBase:
type: object
required:
- id
- type
- createdAt
properties:
id:
type: string
format: uuid
type:
type: string
createdAt:
type: string
format: date-time
OrderCompletedEvent:
allOf:
- $ref: '#/components/schemas/WebhookEventBase'
- type: object
required:
- data
properties:
type:
type: string
const: order.completed
data:
type: object
properties:
orderId:
type: string
format: uuid
total:
type: number
format: double
currency:
type: string
example: USD
```
Document a shared `WebhookEventBase` so all event payloads have a consistent
envelope with `id`, `type`, and `createdAt`.
---
## Best Practices
1. **Use consistent, plural resource names.** `/users`, `/orders`, `/invoices`
-- never mix singular and plural within the same API.
2. **Make mutating operations idempotent.** Accept an `Idempotency-Key` header
on POST endpoints so clients can safely retry without creating duplicates.
3. **Return rate-limit headers on every response.** Include `X-RateLimit-Limit`,
`X-RateLimit-Remaining`, and `X-RateLimit-Reset` so clients can self-throttle.
4. **Provide `operationId` for every operation.** Code generators use this as
the method name; without it, generated clients have meaningless names.
5. **Include realistic examples in the spec.** Examples power documentation UIs,
mock servers, and contract tests. Add them at both the schema and operation
level.
6. **Use `additionalProperties: false` on request schemas.** This catches typos
in client payloads early and prevents silently ignored fields.
7. **Document hypermedia links (HATEOAS basics).** Even a minimal `_links`
object with `self` and `next` URIs helps clients navigate without hardcoding
paths.
8. **Version your spec file alongside code.** Store the OpenAPI document in the
same repository as the implementation. Run a CI check (e.g., `redocly lint`)
to validate the spec on every pull request.
---
## Common Pitfalls
1. **Missing error documentation.** Every operation should list its possible
`4xx` and `5xx` responses. Consumers cannot handle errors they do not know
about. At minimum document `400`, `401`, `403`, `404`, and `500`.
2. **Overusing `200 OK` for everything.** Return `201` for resource creation,
`204` for deletion, and `202` for asynchronous actions. Correct status codes
let generic HTTP clients behave properly (e.g., following `Location` headers).
3. **Deeply nested resource URLs.** `/users/{uid}/orders/{oid}/items/{iid}/notes`
is fragile and hard to cache. Flatten to `/order-items/{iid}/notes` once the
relationship is established.
4. **Inconsistent naming conventions.** Mixing `camelCase` and `snake_case`
within the same API confuses consumers. Pick one JSON field casing and enforce
it with a linter rule.
5. **Ignoring `nullable` vs optional.** In OpenAPI 3.1, `nullable` is gone;
use `type: ["string", "null"]` instead. A field that is not in `required`
may be absent, but that is different from being explicitly `null`. Be precise
about which you intend.
6. **No pagination on list endpoints.** Returning unbounded arrays will
eventually cause timeouts or OOM errors. Every collection endpoint should
accept `limit` and either `cursor` or `offset` from day one, even if the
dataset is currently small.
---
## Related Skills
- `patterns/api-client` - Patterns for consuming and generating API clients from specs
- `patterns/error-handling` - Consistent error response structures and handling
- `frameworks/fastapi` - FastAPI framework with built-in OpenAPI generation
@@ -1,240 +0,0 @@
openapi: "3.1.0"
info:
title: My API
description: Starter API specification. Replace with your project details.
version: "1.0.0"
contact:
name: API Support
email: support@example.com
servers:
- url: http://localhost:3000/api/v1
description: Local development
- url: https://api.example.com/v1
description: Production
tags:
- name: Users
description: User management
- name: Health
description: Service health checks
paths:
/health:
get:
tags: [Health]
summary: Health check
operationId: getHealth
responses:
"200":
description: Service is healthy
content:
application/json:
schema:
type: object
properties:
status: { type: string, example: ok }
timestamp: { type: string, format: date-time }
/users:
get:
tags: [Users]
summary: List users
operationId: listUsers
security: [{ bearerAuth: [] }]
parameters:
- $ref: "#/components/parameters/PageParam"
- $ref: "#/components/parameters/PerPageParam"
- $ref: "#/components/parameters/SortParam"
- name: status
in: query
schema: { type: string, enum: [active, inactive] }
responses:
"200":
description: Paginated list of users
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/User" }
pagination: { $ref: "#/components/schemas/Pagination" }
"401": { $ref: "#/components/responses/Unauthorized" }
post:
tags: [Users]
summary: Create a user
operationId: createUser
security: [{ bearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/CreateUserRequest" }
responses:
"201":
description: User created
headers:
Location: { schema: { type: string }, description: URL of created user }
content:
application/json:
schema: { $ref: "#/components/schemas/User" }
"400": { $ref: "#/components/responses/BadRequest" }
"401": { $ref: "#/components/responses/Unauthorized" }
"422": { $ref: "#/components/responses/ValidationError" }
/users/{userId}:
parameters:
- name: userId
in: path
required: true
schema: { type: string }
get:
tags: [Users]
summary: Get a user by ID
operationId: getUser
security: [{ bearerAuth: [] }]
responses:
"200":
description: User details
content:
application/json:
schema: { $ref: "#/components/schemas/User" }
"401": { $ref: "#/components/responses/Unauthorized" }
"404": { $ref: "#/components/responses/NotFound" }
patch:
tags: [Users]
summary: Update a user
operationId: updateUser
security: [{ bearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/UpdateUserRequest" }
responses:
"200":
description: User updated
content:
application/json:
schema: { $ref: "#/components/schemas/User" }
"401": { $ref: "#/components/responses/Unauthorized" }
"404": { $ref: "#/components/responses/NotFound" }
"422": { $ref: "#/components/responses/ValidationError" }
delete:
tags: [Users]
summary: Delete a user
operationId: deleteUser
security: [{ bearerAuth: [] }]
responses:
"204": { description: User deleted }
"401": { $ref: "#/components/responses/Unauthorized" }
"404": { $ref: "#/components/responses/NotFound" }
components:
securitySchemes:
bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
apiKeyAuth: { type: apiKey, in: header, name: X-API-Key }
parameters:
PageParam:
name: page
in: query
schema: { type: integer, minimum: 1, default: 1 }
PerPageParam:
name: per_page
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
SortParam:
name: sort
in: query
description: "Field to sort by. Prefix with - for descending."
schema: { type: string, example: "-created_at" }
schemas:
User:
type: object
required: [id, email, name, status, created_at]
properties:
id: { type: string, example: usr_abc123 }
email: { type: string, format: email, example: jane@example.com }
name: { type: string, example: Jane Doe }
status: { type: string, enum: [active, inactive] }
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
CreateUserRequest:
type: object
required: [email, name]
properties:
email: { type: string, format: email }
name: { type: string, minLength: 1, maxLength: 100 }
role: { type: string, enum: [user, admin], default: user }
UpdateUserRequest:
type: object
properties:
name: { type: string, minLength: 1, maxLength: 100 }
status: { type: string, enum: [active, inactive] }
Pagination:
type: object
properties:
page: { type: integer }
per_page: { type: integer }
total: { type: integer }
total_pages: { type: integer }
Error:
type: object
required: [error]
properties:
error:
type: object
required: [code, message]
properties:
code: { type: string }
message: { type: string }
details:
type: array
items:
type: object
properties:
field: { type: string }
message: { type: string }
request_id: { type: string }
responses:
BadRequest:
description: Bad request
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
example: { error: { code: BAD_REQUEST, message: Malformed request body } }
Unauthorized:
description: Authentication required
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
example: { error: { code: UNAUTHORIZED, message: Missing or invalid token } }
NotFound:
description: Resource not found
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
example: { error: { code: NOT_FOUND, message: Resource not found } }
ValidationError:
description: Validation failed
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
example:
error:
code: VALIDATION_ERROR
message: Request validation failed
details: [{ field: email, message: Email already registered }]
+62
View File
@@ -0,0 +1,62 @@
---
name: authentication
description: >
Use when implementing JWT tokens, OAuth2 flows, session management, role-based access control (RBAC), password hashing, or multi-factor authentication. Also activate whenever code handles login, signup, token refresh, protected routes, permission checks, or user identity verification. Applies to middleware auth guards and API key authentication.
---
# Authentication & Authorization
## When to Use
- Implementing JWT creation, verification, and refresh token flows
- Building OAuth2 authorization code or PKCE flows
- Password hashing with argon2 or bcrypt
- Role-based access control (RBAC) or permission checks
- Session management with Redis or database-backed sessions
- API key authentication for service-to-service communication
- Multi-factor authentication (TOTP, SMS, email)
## When NOT to Use
- Token-free static sites or public APIs with no auth requirements
- Third-party auth services where implementation is fully managed (Auth0, Clerk) — unless customizing
- Simple scripts or CLI tools that do not need user identity
---
## Quick Reference
| Topic | Reference | Key content |
|-------|-----------|-------------|
| All auth patterns | `references/patterns.md` | JWT, OAuth2, password hashing, RBAC, sessions, API keys |
| Auth flow diagrams | `references/auth-flows.md` | Visual flow diagrams for OAuth2, JWT refresh, session lifecycle |
---
## Best Practices
1. **Never store passwords in plain text.** Use argon2id (preferred) or bcrypt with a work factor of 12+.
2. **Keep JWT tokens short-lived.** Access tokens should expire in 15-30 minutes. Use refresh tokens for longer sessions.
3. **Validate tokens on every request.** Never trust a token without verifying signature, expiration, and issuer.
4. **Use HttpOnly, Secure, SameSite cookies** for web session tokens. Never store tokens in localStorage.
5. **Implement token refresh rotation.** Invalidate old refresh tokens when a new one is issued to detect token theft.
6. **Separate authentication from authorization.** Auth verifies identity; authz checks permissions. Keep them in separate middleware/guards.
7. **Rate limit auth endpoints.** Login, registration, and password reset endpoints are prime brute-force targets.
8. **Log auth events.** Record login attempts (success and failure), token refreshes, and permission denials for security auditing.
## Common Pitfalls
1. **Storing JWTs in localStorage** — vulnerable to XSS. Use HttpOnly cookies instead.
2. **Not rotating refresh tokens** — a stolen refresh token gives permanent access.
3. **Hardcoding secrets** — JWT signing keys and API keys must come from environment variables.
4. **Missing token expiration checks** — always verify `exp` claim server-side.
5. **Overly broad RBAC roles** — prefer granular permissions over a few broad roles.
6. **Not hashing API keys** — store hashed API keys in the database, not plain text.
---
## Related Skills
- `owasp` — Security vulnerabilities in auth flows
- `backend-frameworks` — Framework-specific auth middleware
- `databases` — Storing user credentials and sessions
@@ -1,8 +1,5 @@
---
name: authentication
description: >
Authentication and authorization patterns for web applications. Use this skill when implementing JWT tokens, OAuth2 flows, session management, role-based access control (RBAC), password hashing, or multi-factor authentication. Trigger whenever code handles login, signup, token refresh, protected routes, permission checks, or user identity verification. Also applies to middleware auth guards and API key authentication.
---
# Authentication — Patterns
# Authentication & Authorization Patterns
@@ -853,7 +850,7 @@ async def verify_mfa(payload: MfaVerifyRequest, response: Response):
## Related Skills
- `security/owasp` - OWASP Top 10 security patterns and secure coding practices
- `patterns/api-client` - HTTP client patterns including auth token injection and refresh flows
- `frameworks/fastapi` - FastAPI-specific dependency injection and middleware patterns
- `frameworks/nextjs` - Next.js middleware and route protection patterns
- `owasp` - OWASP Top 10 security patterns and secure coding practices
- `api-client` - HTTP client patterns including auth token injection and refresh flows
- `fastapi` - FastAPI-specific dependency injection and middleware patterns
- `nextjs` - Next.js middleware and route protection patterns
@@ -0,0 +1,65 @@
---
name: backend-frameworks
description: >
Use when building REST APIs or web servers with FastAPI, Django, NestJS, or Express — including routing, middleware, dependency injection, Pydantic models, serializers, controllers, services, guards, pipes, app.get, app.post, APIRouter, class-based views, or framework-specific patterns.
---
# Backend Frameworks
## When to Use
- Building REST APIs with FastAPI, Django REST Framework, NestJS, or Express
- Configuring middleware, routing, authentication, or request validation
- Setting up dependency injection, services, or module structure
- Integrating with databases via ORMs (SQLAlchemy, Django ORM, TypeORM, Prisma)
- WebSocket servers, microservices, or GraphQL resolvers
## When NOT to Use
- Frontend development — use `frontend`
- Database-specific queries without framework context — use `databases`
- API design/documentation — use `openapi`
---
## Quick Reference
| Framework | Reference | Language | Key features |
|-----------|-----------|----------|-------------|
| FastAPI | `references/fastapi.md` | Python | Pydantic, async, APIRouter, Depends(), OpenAPI auto-docs |
| Django | `references/django.md` | Python | ORM, admin, DRF serializers, class-based views, migrations |
| NestJS | `references/nestjs.md` | TypeScript | Modules, DI, guards, pipes, interceptors, Prisma/TypeORM |
| Express | `references/express.md` | TypeScript | Middleware, Router, error handling, helmet, rate limiting |
---
## Best Practices
1. **Use validation models for all request/response data.** Pydantic (FastAPI), class-validator DTOs (NestJS), Zod (Express), serializers (Django).
2. **Separate business logic from routes/controllers.** Route handlers handle HTTP; services handle domain logic.
3. **Organize routes by resource and version.** APIRouter (FastAPI), module structure (NestJS), Router (Express), URL conf (Django).
4. **Return proper HTTP status codes.** 201 for creation, 204 for deletion, 202 for accepted-but-not-done, 409 for conflicts.
5. **Use async all the way down.** Never mix sync blocking calls in async routes (especially FastAPI).
6. **Configure settings from environment variables.** pydantic-settings (FastAPI), django-environ (Django), dotenv (Express/NestJS).
7. **Use `select_related`/`prefetch_related` for every query touching relations** (Django).
8. **Use `transaction.atomic()` for multi-step writes** (Django).
## Common Pitfalls
1. **Blocking I/O in async routes.** `requests.get()`, `time.sleep()` in `async def` routes starves the event loop (FastAPI).
2. **Missing response_model / leaking internal fields** (FastAPI).
3. **N+1 queries from missing eager loading** (Django `select_related`, NestJS relations).
4. **Circular imports/dependencies.** Use `forwardRef()` (NestJS), restructure modules (Django/FastAPI).
5. **Forgetting `asyncHandler`** — unhandled promise rejections crash the process (Express).
6. **Error handler not registered last** (Express).
7. **Putting business logic in controllers** (NestJS).
8. **Not using `whitelist: true` on ValidationPipe** (NestJS).
---
## Related Skills
- `databases` — Database queries, schema design, migrations
- `openapi` — API specification and documentation
- `error-handling` — Exception handling and API error responses
- `authentication` — Auth flows for web applications
@@ -1,8 +1,5 @@
---
name: django
description: >
Use this skill when working with Django web applications, Django ORM models, Django REST Framework APIs, or Django admin interfaces. Trigger for any mention of Django views, serializers, migrations, URL routing, class-based views, or Django middleware. Also applies when building Python web apps with templates, managing database schemas through Django migrations, or setting up admin panels.
---
# Backend Frameworks — Django Patterns
# Django
@@ -15,7 +12,7 @@ description: >
## When NOT to Use
- FastAPI projects — use the `frameworks/fastapi` skill instead for async APIs and microservices
- FastAPI projects — use the `fastapi` skill instead for async APIs and microservices
- JavaScript/Node.js backends (Express, NestJS) — this skill is Python-only
- Microservices architectures — consider FastAPI instead for lightweight, async services
@@ -710,6 +707,6 @@ def transfer_project(project, new_owner):
## Related Skills
- `languages/python` — Python language patterns and best practices
- `databases/postgresql` — Database integration and query optimization
- `testing/pytest` — Testing Django applications with pytest-django
- `python` — Python language patterns and best practices
- `postgresql` — Database integration and query optimization
- `pytest` — Testing Django applications with pytest-django
@@ -0,0 +1,451 @@
# Backend Frameworks — Express Patterns
# Express
## Overview
Production patterns for building Node.js HTTP servers and REST APIs with Express. Covers routing, middleware, validation, error handling, authentication, database integration, and testing.
## When to Use
- Building REST APIs with Express (without NestJS)
- Adding middleware (auth, logging, rate limiting, CORS)
- Handling file uploads, streaming, or WebSockets on Express
- Migrating Express apps or adding features to existing ones
## When NOT to Use
- **NestJS projects** — use the `nestjs` skill (NestJS wraps Express but has its own patterns)
- **FastAPI / Django** — use the `fastapi` or `django` skill
- **Frontend** — use `react` or `nextjs`
- **Cloudflare Workers / edge** — use `cloudflare-workers`
---
## Quick Reference
| I need... | Go to |
|-----------|-------|
| Project structure | SS Architecture below |
| Route patterns | SS Routing below |
| Middleware | SS Middleware below |
| Input validation | SS Validation below |
| Error handling | SS Error Handling below |
| Auth patterns | SS Authentication below |
| Database integration | SS Database below |
| Testing | SS Testing below |
---
## Architecture
### Project structure
```
src/
├── app.ts # Express app setup (middleware, routes)
├── server.ts # HTTP server bootstrap
├── routes/
│ ├── index.ts # Route aggregator
│ ├── users.routes.ts # /api/users
│ └── orders.routes.ts # /api/orders
├── middleware/
│ ├── auth.ts # JWT verification
│ ├── validate.ts # Zod validation middleware
│ ├── error-handler.ts # Global error handler
│ └── rate-limit.ts # Rate limiting
├── services/
│ ├── users.service.ts # Business logic
│ └── orders.service.ts
├── models/ # Prisma or TypeORM entities
├── utils/
│ └── async-handler.ts # Async error wrapper
└── tests/
├── users.test.ts
└── orders.test.ts
```
### App setup
```typescript
// src/app.ts
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import { json } from 'express';
import { router } from './routes';
import { errorHandler } from './middleware/error-handler';
const app = express();
// Security middleware
app.use(helmet());
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
// Body parsing
app.use(json({ limit: '10kb' }));
// Routes
app.use('/api', router);
// Health check
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
// Global error handler (must be last)
app.use(errorHandler);
export { app };
```
```typescript
// src/server.ts
import { app } from './app';
const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => console.log(`Listening on :${PORT}`));
```
---
## Routing
### Router pattern
```typescript
// src/routes/users.routes.ts
import { Router } from 'express';
import { UsersService } from '../services/users.service';
import { validate } from '../middleware/validate';
import { createUserSchema, updateUserSchema } from '../schemas/user.schema';
import { asyncHandler } from '../utils/async-handler';
const router = Router();
const service = new UsersService();
router.post('/', validate(createUserSchema), asyncHandler(async (req, res) => {
const user = await service.create(req.body);
res.status(201).json(user);
}));
router.get('/:id', asyncHandler(async (req, res) => {
const user = await service.findOne(req.params.id);
if (!user) {
res.status(404).json({ type: 'not-found', title: 'Not Found', status: 404, detail: `User ${req.params.id} not found` });
return;
}
res.json(user);
}));
router.patch('/:id', validate(updateUserSchema), asyncHandler(async (req, res) => {
const user = await service.update(req.params.id, req.body);
res.json(user);
}));
router.delete('/:id', asyncHandler(async (req, res) => {
await service.remove(req.params.id);
res.status(204).end();
}));
export { router as usersRouter };
```
### Async error wrapper
```typescript
// src/utils/async-handler.ts
import { Request, Response, NextFunction, RequestHandler } from 'express';
export function asyncHandler(
fn: (req: Request, res: Response, next: NextFunction) => Promise<void>
): RequestHandler {
return (req, res, next) => fn(req, res, next).catch(next);
}
```
### Route aggregator
```typescript
// src/routes/index.ts
import { Router } from 'express';
import { usersRouter } from './users.routes';
import { ordersRouter } from './orders.routes';
const router = Router();
router.use('/users', usersRouter);
router.use('/orders', ordersRouter);
export { router };
```
---
## Middleware
### Middleware order matters
```typescript
// Correct order in app.ts:
app.use(helmet()); // 1. Security headers
app.use(cors()); // 2. CORS
app.use(json()); // 3. Body parsing
app.use(requestLogger); // 4. Logging
app.use(rateLimiter); // 5. Rate limiting
app.use('/api', router); // 6. Routes
app.use(errorHandler); // 7. Error handler (MUST be last)
```
### Request logging
```typescript
import { Request, Response, NextFunction } from 'express';
export function requestLogger(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
res.on('finish', () => {
console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${Date.now() - start}ms`);
});
next();
}
```
### Rate limiting
```typescript
import rateLimit from 'express-rate-limit';
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
message: { type: 'rate-limit', title: 'Too Many Requests', status: 429 },
});
```
---
## Validation
### Zod validation middleware
```typescript
// src/middleware/validate.ts
import { Request, Response, NextFunction } from 'express';
import { ZodSchema, ZodError } from 'zod';
export function validate(schema: ZodSchema) {
return (req: Request, res: Response, next: NextFunction) => {
try {
req.body = schema.parse(req.body);
next();
} catch (err) {
if (err instanceof ZodError) {
res.status(400).json({
type: 'validation-error',
title: 'Bad Request',
status: 400,
detail: err.errors.map(e => `${e.path.join('.')}: ${e.message}`).join('; '),
});
return;
}
next(err);
}
};
}
```
```typescript
// src/schemas/user.schema.ts
import { z } from 'zod';
export const createUserSchema = z.object({
email: z.string().email().max(254),
name: z.string().min(1).max(100),
role: z.enum(['admin', 'member', 'viewer']).default('member'),
});
export const updateUserSchema = createUserSchema.partial();
export type CreateUserInput = z.infer<typeof createUserSchema>;
```
---
## Error Handling
### Global error handler (RFC 9457 Problem Details)
```typescript
// src/middleware/error-handler.ts
import { Request, Response, NextFunction } from 'express';
export class AppError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
}
}
export function errorHandler(err: Error, _req: Request, res: Response, _next: NextFunction) {
const status = err instanceof AppError ? err.statusCode : 500;
const title = status >= 500 ? 'Internal Server Error' : err.message;
if (status >= 500) console.error(err);
res.status(status).json({
type: `https://api.example.com/problems/${status}`,
title,
status,
detail: status >= 500 ? 'An unexpected error occurred' : err.message,
});
}
```
---
## Authentication
### JWT middleware
```typescript
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
export interface AuthRequest extends Request {
user?: { sub: string; role: string };
}
export function authenticate(req: AuthRequest, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
res.status(401).json({ type: 'unauthorized', title: 'Unauthorized', status: 401, detail: 'Missing bearer token' });
return;
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as AuthRequest['user'];
req.user = payload;
next();
} catch {
res.status(401).json({ type: 'unauthorized', title: 'Unauthorized', status: 401, detail: 'Invalid or expired token' });
}
}
export function authorize(...roles: string[]) {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (!req.user || !roles.includes(req.user.role)) {
res.status(403).json({ type: 'forbidden', title: 'Forbidden', status: 403, detail: 'Insufficient permissions' });
return;
}
next();
};
}
```
---
## Database
### Prisma integration
```typescript
// src/db.ts
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();
// Graceful shutdown
process.on('SIGTERM', async () => {
await prisma.$disconnect();
process.exit(0);
});
```
```typescript
// src/services/users.service.ts
import { prisma } from '../db';
import { CreateUserInput } from '../schemas/user.schema';
export class UsersService {
async findOne(id: string) {
return prisma.user.findUnique({ where: { id } });
}
async create(data: CreateUserInput) {
return prisma.user.create({ data });
}
async update(id: string, data: Partial<CreateUserInput>) {
return prisma.user.update({ where: { id }, data });
}
async remove(id: string) {
await prisma.user.delete({ where: { id } });
}
}
```
---
## Testing
### Integration tests with supertest
```typescript
// src/tests/users.test.ts
import request from 'supertest';
import { app } from '../app';
import { prisma } from '../db';
describe('Users API', () => {
afterAll(() => prisma.$disconnect());
it('POST /api/users — creates user', async () => {
const res = await request(app)
.post('/api/users')
.send({ email: 'test@example.com', name: 'Test' })
.expect(201);
expect(res.body).toHaveProperty('id');
expect(res.body.email).toBe('test@example.com');
});
it('POST /api/users — rejects invalid email', async () => {
await request(app)
.post('/api/users')
.send({ email: 'not-an-email', name: 'Test' })
.expect(400);
});
it('GET /api/users/:id — returns 404 for missing user', async () => {
await request(app)
.get('/api/users/nonexistent')
.expect(404);
});
});
```
---
## Common Pitfalls
1. **Forgetting `asyncHandler`.** Unhandled promise rejections crash the process. Wrap every async route handler.
2. **Error handler not last.** Express error handlers must have 4 parameters `(err, req, res, next)` and must be registered after all routes.
3. **Not calling `next()`.** Middleware that doesn't call `next()` or send a response will hang the request.
4. **Mutating `req.body` without validation.** Always validate before trusting input. Use Zod or Joi middleware.
5. **Hardcoding CORS origin.** Use environment variables for allowed origins. Never use `cors({ origin: '*' })` in production.
6. **Missing `helmet()`.** Always use helmet for security headers. It's one line and prevents common attacks.
7. **Not limiting body size.** Use `json({ limit: '10kb' })` to prevent denial-of-service via large payloads.
8. **Using `express.static` for uploads.** Serve user uploads from a CDN or S3, not from the Express process.
---
## Related Skills
- `nestjs` — If you need DI, decorators, and modules, use NestJS instead of raw Express
- `openapi` — OpenAPI spec design for Express APIs (use `swagger-jsdoc` + `swagger-ui-express`)
- `typescript` — TypeScript patterns (Express is typed via `@types/express`)
- `docker` — Containerizing Express apps
- `authentication` — JWT / OAuth2 patterns (framework-agnostic)
- `jest` — Testing Express with Jest + supertest
@@ -1,8 +1,5 @@
---
name: fastapi
description: >
Use this skill when building REST APIs with Python and FastAPI, creating async web applications, or generating OpenAPI/Swagger documentation. Trigger for any mention of FastAPI, Pydantic models, async Python endpoints, dependency injection in Python APIs, or APIRouter patterns. Also applies when setting up Python microservices, adding request validation with Pydantic, or configuring ASGI applications.
---
# Backend Frameworks — FastAPI Patterns
# FastAPI
@@ -16,7 +13,7 @@ description: >
## When NOT to Use
- Django projects — use the `frameworks/django` skill instead
- Django projects — use the `django` skill instead
- JavaScript/Node.js backends (Express, NestJS) — this skill is Python-only
- Non-API applications such as CLI tools, desktop apps, or batch processing scripts
@@ -672,9 +669,9 @@ settings = Settings()
## Related Skills
- `languages/python` — Python language patterns and best practices
- `api/openapi` — OpenAPI specification and documentation standards
- `databases/postgresql` — Database integration with async SQLAlchemy
- `testing/pytest` — Testing FastAPI applications with pytest and httpx
- `patterns/authentication` — JWT, OAuth2, and session patterns for FastAPI endpoints
- `patterns/logging` — Structured logging for FastAPI applications
- `python` — Python language patterns and best practices
- `openapi` — OpenAPI specification and documentation standards
- `postgresql` — Database integration with async SQLAlchemy
- `pytest` — Testing FastAPI applications with pytest and httpx
- `authentication` — JWT, OAuth2, and session patterns for FastAPI endpoints
- `logging` — Structured logging for FastAPI applications
@@ -0,0 +1,660 @@
# Backend Frameworks — NestJS Patterns
# NestJS
## Overview
Production patterns for building TypeScript backend APIs with NestJS. Covers module architecture, dependency injection, request validation, authentication guards, database integration, testing, and deployment.
## When to Use
- Building REST APIs or GraphQL servers with NestJS
- Configuring modules, providers, and dependency injection
- Creating guards, interceptors, pipes, or middleware
- Integrating Prisma, TypeORM, or MikroORM with NestJS
- Building microservices or WebSocket gateways
- Testing NestJS controllers and services
## When NOT to Use
- **Express without NestJS** — use `express` patterns directly
- **FastAPI / Django** — use the `fastapi` or `django` skill
- **Frontend** — use `react` or `nextjs`
- **Simple scripts** — NestJS is overkill for one-file utilities
---
## Quick Reference
| I need... | Go to |
|-----------|-------|
| Module/DI patterns | § Architecture below |
| Request validation | § Pipes & Validation below |
| Auth guards (JWT/API key) | § Authentication below |
| Database integration | § Database below |
| Testing patterns | § Testing below |
| Error handling | § Exception Filters below |
| OpenAPI generation | § OpenAPI below |
---
## Architecture
### Module structure
Every NestJS app is a tree of modules. Keep modules focused on a single domain.
```
src/
├── app.module.ts # Root module — imports feature modules
├── main.ts # Bootstrap
├── common/ # Shared utilities
│ ├── decorators/
│ ├── filters/
│ ├── guards/
│ ├── interceptors/
│ └── pipes/
├── config/ # Configuration module
│ ├── config.module.ts
│ └── config.service.ts
├── users/ # Feature module
│ ├── users.module.ts
│ ├── users.controller.ts
│ ├── users.service.ts
│ ├── dto/
│ │ ├── create-user.dto.ts
│ │ └── update-user.dto.ts
│ ├── entities/
│ │ └── user.entity.ts
│ └── users.controller.spec.ts
└── orders/ # Another feature module
├── orders.module.ts
├── orders.controller.ts
└── orders.service.ts
```
### Module pattern
```typescript
// users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // Expose to other modules
})
export class UsersModule {}
```
### Controller + Service pattern
```typescript
// users/users.controller.ts
import { Controller, Get, Post, Body, Param, Patch, Delete, HttpCode, HttpStatus } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
return this.usersService.update(id, dto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id') id: string) {
return this.usersService.remove(id);
}
}
```
```typescript
// users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
@Injectable()
export class UsersService {
async findOne(id: string) {
const user = await this.prisma.user.findUnique({ where: { id } });
if (!user) throw new NotFoundException(`User ${id} not found`);
return user;
}
async create(dto: CreateUserDto) {
return this.prisma.user.create({ data: dto });
}
async update(id: string, dto: UpdateUserDto) {
await this.findOne(id); // throws if missing
return this.prisma.user.update({ where: { id }, data: dto });
}
async remove(id: string) {
await this.findOne(id);
await this.prisma.user.delete({ where: { id } });
}
}
```
---
## Pipes & Validation
Use `class-validator` + `class-transformer` with the global `ValidationPipe`.
### Bootstrap validation globally
```typescript
// main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // Strip unknown properties
forbidNonWhitelisted: true, // Reject unknown properties with 400
transform: true, // Auto-transform payloads to DTO instances
transformOptions: { enableImplicitConversion: true },
}));
await app.listen(3000);
}
bootstrap();
```
### DTO with validation
```typescript
// users/dto/create-user.dto.ts
import { IsEmail, IsString, MinLength, MaxLength, IsOptional, IsEnum } from 'class-validator';
export class CreateUserDto {
@IsEmail()
@MaxLength(254)
email: string;
@IsString()
@MinLength(1)
@MaxLength(100)
name: string;
@IsOptional()
@IsEnum(['admin', 'member', 'viewer'])
role?: string = 'member';
}
```
```typescript
// users/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
```
`PartialType` makes all fields optional and preserves validators — no manual duplication.
---
## Authentication
### JWT Guard
```typescript
// common/guards/jwt-auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private readonly jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractToken(request);
if (!token) throw new UnauthorizedException('Missing bearer token');
try {
const payload = await this.jwtService.verifyAsync(token);
request['user'] = payload;
} catch {
throw new UnauthorizedException('Invalid or expired token');
}
return true;
}
private extractToken(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
```
### Apply guard globally with public route bypass
```typescript
// app.module.ts
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
@Module({
providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }],
})
export class AppModule {}
```
```typescript
// common/decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
```
```typescript
// In JwtAuthGuard.canActivate(), add at the top:
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
```
```typescript
// Usage — mark public routes
@Public()
@Get('health')
health() { return { status: 'ok' }; }
```
### Role-based access
```typescript
// common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
// common/guards/roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(), context.getClass(),
]);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.includes(user.role);
}
}
```
---
## Exception Filters
### Global Problem Details filter (RFC 9457)
Consistent with the `openapi` skill's convention — all errors as `application/problem+json`.
```typescript
// common/filters/problem-details.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
@Catch()
export class ProblemDetailsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const exceptionResponse = exception instanceof HttpException
? exception.getResponse()
: {};
const detail = typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message;
response.status(status).json({
type: `https://api.example.com/problems/${this.slugify(status)}`,
title: HttpStatus[status]?.replace(/_/g, ' ').toLowerCase() ?? 'Error',
status,
detail: Array.isArray(detail) ? detail.join('; ') : detail,
});
}
private slugify(status: number): string {
return (HttpStatus[status] ?? 'error').toLowerCase().replace(/_/g, '-');
}
}
```
Register globally in `main.ts`:
```typescript
app.useGlobalFilters(new ProblemDetailsFilter());
```
---
## Database
### Prisma integration (recommended)
```typescript
// prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit() { await this.$connect(); }
async onModuleDestroy() { await this.$disconnect(); }
}
```
```typescript
// prisma/prisma.module.ts
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
```
Then inject `PrismaService` in any service:
```typescript
@Injectable()
export class UsersService {
constructor(private readonly prisma: PrismaService) {}
// ...
}
```
### TypeORM alternative
```typescript
// users/entities/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column()
name: string;
@Column({ default: 'member' })
role: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
```
---
## Testing
### Unit testing a service
```typescript
// users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { PrismaService } from '../prisma/prisma.service';
import { NotFoundException } from '@nestjs/common';
describe('UsersService', () => {
let service: UsersService;
let prisma: PrismaService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: PrismaService,
useValue: {
user: {
findUnique: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
},
},
],
}).compile();
service = module.get(UsersService);
prisma = module.get(PrismaService);
});
it('throws NotFoundException when user does not exist', async () => {
jest.spyOn(prisma.user, 'findUnique').mockResolvedValue(null);
await expect(service.findOne('missing')).rejects.toThrow(NotFoundException);
});
});
```
### E2E testing a controller
```typescript
// test/users.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('UsersController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = module.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));
await app.init();
});
afterAll(() => app.close());
it('POST /users — creates user', () =>
request(app.getHttpServer())
.post('/users')
.send({ email: 'test@example.com', name: 'Test' })
.expect(201)
.expect((res) => {
expect(res.body).toHaveProperty('id');
expect(res.body.email).toBe('test@example.com');
}));
it('POST /users — rejects invalid email', () =>
request(app.getHttpServer())
.post('/users')
.send({ email: 'not-an-email', name: 'Test' })
.expect(400));
});
```
---
## OpenAPI
NestJS has first-class OpenAPI generation via `@nestjs/swagger`.
```typescript
// main.ts
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
const config = new DocumentBuilder()
.setTitle('Acme API')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);
```
Annotate DTOs for richer specs:
```typescript
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({ example: 'jane@example.com', maxLength: 254 })
@IsEmail()
email: string;
@ApiPropertyOptional({ enum: ['admin', 'member', 'viewer'], default: 'member' })
@IsOptional()
@IsEnum(['admin', 'member', 'viewer'])
role?: string = 'member';
}
```
Use the `@nestjs/swagger` CLI plugin to auto-generate `@ApiProperty` decorators from TypeScript types — saves boilerplate.
---
## Configuration
Use `@nestjs/config` with Zod validation for type-safe env vars.
```typescript
// config/env.validation.ts
import { z } from 'zod';
export const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
});
export type Env = z.infer<typeof envSchema>;
```
```typescript
// config/config.module.ts
import { ConfigModule } from '@nestjs/config';
import { envSchema } from './env.validation';
export const AppConfigModule = ConfigModule.forRoot({
validate: (config) => envSchema.parse(config),
isGlobal: true,
});
```
---
## Interceptors
### Request logging
```typescript
// common/interceptors/logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const { method, url } = req;
const start = Date.now();
return next.handle().pipe(
tap(() => {
const res = context.switchToHttp().getResponse();
this.logger.log(`${method} ${url} ${res.statusCode} ${Date.now() - start}ms`);
}),
);
}
}
```
### Response transform (envelope)
```typescript
// common/interceptors/transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, map } from 'rxjs';
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, { data: T }> {
intercept(_context: ExecutionContext, next: CallHandler<T>): Observable<{ data: T }> {
return next.handle().pipe(map((data) => ({ data })));
}
}
```
---
## Common Pitfalls
1. **Circular dependencies.** Module A imports Module B which imports Module A. Use `forwardRef()` or restructure to break the cycle. If you need `forwardRef`, the architecture likely needs rethinking.
2. **Not using `whitelist: true` on ValidationPipe.** Without it, extra properties pass through silently — a security risk and a debugging nightmare.
3. **Overusing `@Global()` modules.** Only truly cross-cutting concerns (config, database, logging) should be global. Feature modules should explicitly import what they need.
4. **Testing with real database in unit tests.** Unit tests should mock the database layer. Use NestJS E2E tests (with `supertest`) for integration testing against a real DB.
5. **Forgetting to `await app.init()` in E2E tests.** Without it, providers aren't initialized and tests fail with cryptic DI errors.
6. **Putting business logic in controllers.** Controllers should only parse requests and return responses. All logic goes in services.
7. **Not using `PartialType` / `PickType` / `OmitType` for DTOs.** Duplicating validation rules across create/update DTOs leads to drift. Use mapped types.
8. **Ignoring graceful shutdown.** Call `app.enableShutdownHooks()` so `onModuleDestroy` lifecycle hooks fire on SIGTERM (critical for database connections in containers).
---
## Related Skills
- `openapi` — OpenAPI spec design (NestJS auto-generates from decorators)
- `typescript` — TypeScript patterns (NestJS is TypeScript-first)
- `postgresql` — Database design and query optimization
- `docker` — Containerizing NestJS apps
- `playwright` — E2E testing NestJS APIs through the browser
- `authentication` — JWT / OAuth2 patterns (framework-agnostic)
- `github-actions` — CI/CD for NestJS projects
+333
View File
@@ -0,0 +1,333 @@
---
name: background-jobs
description: >
Use when implementing background task processing, job queues, or async work outside the request/response cycle. Trigger for keywords like Celery, BullMQ, Bull, task queue, background job, worker, cron job, scheduled task, async task, delayed job, or any mention of processing work outside the HTTP request lifecycle. Also activate when sending emails, generating reports, processing uploads, or handling webhooks asynchronously.
---
# Background Jobs
## When to Use
- Sending emails or notifications after an API response
- Processing file uploads (resize images, parse CSVs)
- Generating reports or exports
- Webhook delivery with retries
- Scheduled/cron tasks (cleanup, aggregation)
- Any work that would make an API response too slow (>500ms)
## When NOT to Use
- Simple in-request work under 200ms — just do it inline
- One-off scripts — use a CLI command instead
- Real-time bidirectional communication — use WebSockets or SSE
---
## Python: Celery
### Setup
```python
# src/core/celery.py
from celery import Celery
app = Celery('myapp')
app.config_from_object({
'broker_url': 'redis://localhost:6379/1',
'result_backend': 'redis://localhost:6379/2',
'task_serializer': 'json',
'result_serializer': 'json',
'accept_content': ['json'],
'timezone': 'UTC',
'task_track_started': True,
'task_acks_late': True, # Re-deliver if worker crashes
'worker_prefetch_multiplier': 1, # Fair scheduling
})
```
### Define tasks
```python
# src/tasks/email.py
from src.core.celery import app
@app.task(
bind=True,
max_retries=3,
default_retry_delay=60, # 60s between retries
)
def send_welcome_email(self, user_id: str):
try:
user = get_user(user_id)
mailer.send(to=user.email, template='welcome', context={'name': user.name})
except MailerError as exc:
self.retry(exc=exc)
@app.task(bind=True, max_retries=5, default_retry_delay=300)
def process_upload(self, upload_id: str):
try:
upload = get_upload(upload_id)
result = parse_csv(upload.file_path)
save_results(upload_id, result)
except Exception as exc:
self.retry(exc=exc)
```
### Dispatch from FastAPI
```python
from fastapi import APIRouter, status
from src.tasks.email import send_welcome_email
router = APIRouter()
@router.post("/api/users", status_code=status.HTTP_201_CREATED)
async def create_user(body: CreateUserRequest):
user = await save_user(body)
send_welcome_email.delay(str(user.id)) # Fire and forget
return user
```
### Scheduled tasks (Celery Beat)
```python
# In celery config
app.conf.beat_schedule = {
'cleanup-expired-sessions': {
'task': 'src.tasks.cleanup.cleanup_sessions',
'schedule': 3600.0, # Every hour
},
'daily-report': {
'task': 'src.tasks.reports.generate_daily',
'schedule': crontab(hour=2, minute=0), # 2:00 AM UTC
},
}
```
### Run workers
```bash
# Worker
celery -A src.core.celery worker --loglevel=info --concurrency=4
# Beat scheduler
celery -A src.core.celery beat --loglevel=info
# Both (development only)
celery -A src.core.celery worker --beat --loglevel=info
```
---
## TypeScript: BullMQ
### Setup
```typescript
// src/core/queue.ts
import { Queue, Worker } from 'bullmq';
import Redis from 'ioredis';
const connection = new Redis(process.env.REDIS_URL!, { maxRetriesPerRequest: null });
export function createQueue(name: string) {
return new Queue(name, { connection });
}
export function createWorker<T>(
name: string,
processor: (job: { data: T }) => Promise<void>,
) {
return new Worker<T>(name, async (job) => processor(job), {
connection,
concurrency: 5,
});
}
```
### Define queues and workers
```typescript
// src/queues/email.queue.ts
import { createQueue, createWorker } from '../core/queue';
interface WelcomeEmailJob {
userId: string;
email: string;
name: string;
}
export const emailQueue = createQueue('email');
export const emailWorker = createWorker<WelcomeEmailJob>('email', async (job) => {
await mailer.send({
to: job.data.email,
template: 'welcome',
context: { name: job.data.name },
});
});
emailWorker.on('failed', (job, err) => {
console.error(`Email job ${job?.id} failed:`, err.message);
});
```
### Dispatch from NestJS
```typescript
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { emailQueue } from '../queues/email.queue';
@Injectable()
export class UsersService {
async create(dto: CreateUserDto) {
const user = await this.prisma.user.create({ data: dto });
await emailQueue.add('welcome', {
userId: user.id,
email: user.email,
name: user.name,
}, {
attempts: 3,
backoff: { type: 'exponential', delay: 60_000 },
});
return user;
}
}
```
### Scheduled/repeatable jobs
```typescript
// Run every hour
await emailQueue.add('digest', { type: 'hourly' }, {
repeat: { every: 3600_000 },
});
// Cron pattern — daily at 2 AM UTC
await emailQueue.add('daily-report', {}, {
repeat: { pattern: '0 2 * * *' },
});
```
### NestJS module integration
```typescript
// src/queues/queues.module.ts
import { Module, OnModuleDestroy } from '@nestjs/common';
import { emailQueue, emailWorker } from './email.queue';
@Module({
providers: [
{ provide: 'EMAIL_QUEUE', useValue: emailQueue },
],
exports: ['EMAIL_QUEUE'],
})
export class QueuesModule implements OnModuleDestroy {
async onModuleDestroy() {
await emailWorker.close();
await emailQueue.close();
}
}
```
---
## Job Design Patterns
### Idempotent jobs
Jobs may be retried. Design for idempotency:
```python
# BAD — sends duplicate emails on retry
@app.task
def send_email(user_id):
send(user_id)
# GOOD — check before sending
@app.task
def send_email(user_id):
if already_sent(user_id, 'welcome'):
return
send(user_id)
mark_sent(user_id, 'welcome')
```
### Small payloads
```typescript
// BAD — large payload in queue
await queue.add('process', { csvData: '...10MB of CSV...' });
// GOOD — pass a reference
await queue.add('process', { uploadId: 'upload_123' });
```
### Dead letter queues
```typescript
// After max retries, move to DLQ for investigation
await emailQueue.add('welcome', data, {
attempts: 3,
backoff: { type: 'exponential', delay: 60_000 },
removeOnComplete: true,
removeOnFail: false, // Keep failed jobs for inspection
});
```
---
## Testing
### Python (Celery)
```python
# Always run tasks eagerly in tests
@pytest.fixture(autouse=True)
def celery_eager(settings):
settings.CELERY_TASK_ALWAYS_EAGER = True
settings.CELERY_TASK_EAGER_PROPAGATES = True
def test_welcome_email_sent(mock_mailer):
send_welcome_email("user_123")
mock_mailer.send.assert_called_once()
```
### TypeScript (BullMQ)
```typescript
describe('email worker', () => {
it('should send welcome email', async () => {
const sendSpy = vi.spyOn(mailer, 'send');
// Process job directly (skip queue)
await emailWorker.run({ data: { userId: '1', email: 'a@b.com', name: 'Test' } });
expect(sendSpy).toHaveBeenCalledWith(expect.objectContaining({ to: 'a@b.com' }));
});
});
```
---
## Common Pitfalls
1. **Non-idempotent tasks.** Retries will duplicate side effects. Always check before acting.
2. **Large payloads in queue.** Store data in DB/S3, pass only IDs through the queue.
3. **No retry limits.** Always set `max_retries` / `attempts`. Infinite retries waste resources.
4. **Missing dead letter handling.** Failed jobs need investigation. Don't silently discard them.
5. **Blocking the event loop (Node.js).** CPU-heavy work in BullMQ workers blocks other jobs. Use `worker threads` or separate processes.
6. **Not monitoring queue depth.** Queue buildup indicates workers can't keep up. Alert on queue size.
---
## Related Skills
- `redis` — Redis as the message broker for both Celery and BullMQ
- `docker` — Running workers as separate containers
- `fastapi` — Dispatching Celery tasks from FastAPI endpoints
- `nestjs` — BullMQ integration with NestJS modules
- `logging` — Structured logging for job execution tracking
@@ -1,7 +1,7 @@
---
name: brainstorming
description: >
Trigger this skill whenever the user wants to design, explore, or ideate on ANY new feature, architecture decision, or unclear requirement. Activate for keywords like "brainstorm", "design", "explore", "what if", "how should we", "options for", "trade-offs", or any open-ended question about implementation approach. Also trigger when requirements are vague, ambiguous, or when multiple valid solutions exist -- err on the side of brainstorming before jumping into code.
Use when the user wants to design, explore, or ideate on ANY new feature, architecture decision, or unclear requirement. Activate for keywords like "brainstorm", "design", "explore", "what if", "how should we", "options for", "trade-offs", or any open-ended question about implementation approach. Also trigger when requirements are vague, ambiguous, or when multiple valid solutions exist -- err on the side of brainstorming before jumping into code.
---
# Brainstorming
@@ -191,7 +191,65 @@ For informed technology choices:
---
## Stack-Specific Brainstorming Examples
These show what Phase 2 (Exploration) output looks like for different domains:
### FastAPI endpoint design
```markdown
## Approach 1: REST + JWT Bearer Auth (Recommended)
POST /api/orders with Pydantic v2 validation, async SQLAlchemy.
- Pros: Simple, cacheable, great OpenAPI docs via FastAPI
- Cons: Multiple round-trips for nested resources
## Approach 2: GraphQL + API Key Auth
Single /graphql endpoint with Strawberry, API key in header.
- Pros: Flexible queries, single round-trip for nested data
- Cons: Caching harder, team unfamiliar with Strawberry
**Decision**: REST — team knows it, OpenAPI auto-docs save time,
nested resources not needed for this feature.
```
### React data table component
```markdown
## Approach 1: TanStack Table + URL Params (Recommended)
Server component fetches data, client component for interactions.
Sort/filter state in URL search params (shareable links).
- Pros: Bookmarkable state, SSR-friendly, no global store needed
- Cons: URL parsing boilerplate
## Approach 2: Zustand Store + SWR
Client-only with SWR for fetching, Zustand for table state.
- Pros: Simple state management, familiar pattern
- Cons: Not SSR-friendly, state lost on refresh
**Decision**: TanStack Table + URL params — users need to share
filtered views, and it works with Next.js App Router.
```
### Database multi-tenancy
```markdown
## Approach 1: Shared Table + tenant_id + RLS (Recommended)
Single `orders` table with `tenant_id` column, PostgreSQL RLS policies.
- Pros: Simple migrations, single connection pool, no schema sprawl
- Cons: Must never forget WHERE tenant_id = ? (RLS prevents this)
## Approach 2: Schema-per-tenant
Each tenant gets own PostgreSQL schema, selected via search_path.
- Pros: Strong isolation, easy per-tenant backup/restore
- Cons: Migration complexity grows linearly with tenants
**Decision**: Shared table + RLS — we have <100 tenants, RLS gives
isolation guarantees without migration pain.
```
---
## Related Skills
- `methodology/writing-plans` -- After brainstorming produces a validated design, use writing-plans to create a detailed implementation plan
- `methodology/sequential-thinking` -- For complex problems that benefit from structured step-by-step reasoning during the brainstorming process
- `writing-plans` -- After brainstorming produces a validated design, use writing-plans to create a detailed implementation plan
- `sequential-thinking` -- For complex problems that benefit from structured step-by-step reasoning during the brainstorming process
+60
View File
@@ -0,0 +1,60 @@
---
name: caching
description: >
Use when implementing memoization, HTTP cache headers, Redis caching, CDN configuration, or in-memory caches. Also activate whenever code deals with Cache-Control headers, ETags, functools.lru_cache, React useMemo, TanStack Query cache, or any caching strategy. Applies to cache invalidation, TTL policies, and cache-aside patterns.
---
# Caching
## When to Use
- Memoizing expensive function calls (lru_cache, useMemo, node-cache)
- Setting HTTP cache headers (Cache-Control, ETag, Last-Modified)
- Implementing Redis cache-aside pattern for database query results
- Configuring CDN caching for static assets and API responses
- Building multi-layer caches (in-memory + Redis + CDN)
- Implementing cache invalidation strategies
## When NOT to Use
- Data that changes on every request (real-time prices, live feeds)
- Security-sensitive responses that must never be cached (auth tokens, personal data)
- Development environments where stale data causes confusion
---
## Quick Reference
| Topic | Reference | Key content |
|-------|-----------|-------------|
| All caching patterns | `references/patterns.md` | Memoization, HTTP headers, ETags, Redis, CDN, multi-layer, invalidation |
| Decision tree | `references/caching-decision-tree.md` | When to use which caching strategy |
---
## Best Practices
1. **Cache at the right layer.** In-memory for hot paths (<1ms), Redis for shared state (<5ms), CDN for static/semi-static content.
2. **Always set TTLs.** Every cache entry must expire. Unbounded caches grow until they crash.
3. **Use cache-aside (lazy loading) by default.** Read from cache, miss goes to DB, write result to cache. Simplest and most predictable pattern.
4. **Invalidate on write.** When data changes, delete the cache key immediately. Don't wait for TTL expiry.
5. **Use ETag-based validation** for HTTP caching. Cheaper than full responses and guarantees freshness.
6. **Prevent cache stampede.** When a popular key expires, use distributed locks or stale-while-revalidate to prevent all requests from hitting the DB simultaneously.
7. **Monitor cache hit rates.** A cache with <80% hit rate may not be worth the complexity. Measure before optimizing.
## Common Pitfalls
1. **Caching without TTL** — memory grows unboundedly until OOM.
2. **Cache invalidation bugs** — stale data served after writes. Always invalidate on mutation.
3. **Caching user-specific data with shared keys** — one user sees another's data.
4. **Over-caching in development** — confusing stale responses with bugs.
5. **Ignoring serialization costs** — caching large objects in Redis costs more in ser/deser than the DB query saved.
6. **Not handling cache failures gracefully** — if Redis is down, fall through to DB, don't crash.
---
## Related Skills
- `databases` — Redis patterns and database query optimization
- `backend-frameworks` — Framework-specific cache middleware
- `frontend` — React useMemo, TanStack Query cache
@@ -1,8 +1,5 @@
---
name: caching
description: >
Caching patterns for web applications, APIs, and data layers. Use this skill when implementing memoization, HTTP cache headers, Redis caching, CDN configuration, or in-memory caches. Trigger whenever code deals with Cache-Control headers, ETags, functools.lru_cache, React useMemo, TanStack Query cache, or any caching strategy. Also applies to cache invalidation, TTL policies, and cache-aside patterns.
---
# Caching — Patterns
# Caching
@@ -779,8 +776,8 @@ export default async function ProductList({ categoryId }: Props) {
## Related Skills
- `patterns/state-management` — Client-side state management patterns that interact with cache layers
- `databases/postgresql` — Query optimization and connection pooling that complement caching strategies
- `databases/mongodb` — MongoDB query patterns and when to add a cache layer
- `frameworks/nextjs` — Next.js data fetching, ISR, and caching architecture
- `patterns/api-client` — Client-side caching for API responses
- `state-management` — Client-side state management patterns that interact with cache layers
- `postgresql` — Query optimization and connection pooling that complement caching strategies
- `mongodb` — MongoDB query patterns and when to add a cache layer
- `nextjs` — Next.js data fetching, ISR, and caching architecture
- `api-client` — Client-side caching for API responses
@@ -0,0 +1,208 @@
---
name: condition-based-waiting
description: >
Use when waiting on external conditions like CI pipeline runs, deployments, long builds, database migrations, or test suites. Trigger for keywords like "wait for", "check status", "poll", "monitor", "is it done", "build running", "deploy in progress", or when a background process needs to complete before the next step. Also activate when using run_in_background or Monitor tools in Claude Code.
---
# Condition-Based Waiting
## When to Use
- CI/CD pipeline is running and you need results before proceeding
- Deployment is in progress and you need to verify it succeeded
- Long-running build (Next.js, Docker) is executing
- Database migration is applying
- Test suite takes more than 30 seconds
## When NOT to Use
- Commands that complete in under 10 seconds (just run them normally)
- Checking static state that won't change (read the file instead)
- Polling for human action (ask the user instead)
---
## Claude Code Patterns
### Background execution for long commands
Use `run_in_background` when a command takes more than ~30 seconds:
```bash
# Long test suite — run in background, get notified when done
pytest -v --cov=src # run_in_background: true
# Docker build
docker build -t myapp . # run_in_background: true
# Next.js production build
next build # run_in_background: true
# NestJS build + test
npm run build && npm test # run_in_background: true
```
You'll be notified automatically when the command completes — **do not poll or sleep**.
### Monitor tool for streaming output
Use Monitor when you need to watch for specific output patterns:
```bash
# Watch for build completion
until curl -sf http://localhost:3000/health; do sleep 2; done
# Watch for migration completion
until alembic check 2>&1 | grep -q "No new upgrade"; do sleep 5; done
```
---
## Checking CI/CD Status
### GitHub Actions
```bash
# Watch a running workflow (blocks until complete)
gh run watch
# Check status of the latest run
gh run view --json status,conclusion
# Check specific workflow
gh run list --workflow=ci.yml --limit=1 --json status,conclusion
# Wait for all checks on a PR
gh pr checks --watch
```
### After CI completes
```bash
# Get detailed results
gh run view <run-id> --log-failed
# Re-run failed jobs only
gh run rerun <run-id> --failed
```
---
## Checking Deployments
### Health check polling
```bash
# Wait for deployment to be healthy
until curl -sf https://staging.example.com/health | grep -q '"status":"ok"'; do
sleep 5
done
echo "Deployment is healthy"
```
### Vercel / Cloudflare
```bash
# Vercel — check latest deployment status
npx vercel ls --limit=1
# Cloudflare Pages — check deployment
npx wrangler pages deployment list --project-name=myapp
```
---
## Checking Build Output
### Framework-specific patterns
```bash
# Next.js — watch for "Compiled successfully"
# (use run_in_background for `next build`, read output when notified)
# Python — watch for test results
pytest -v --tb=short # run_in_background: true
# Docker — watch for "Successfully built"
docker build -t myapp . # run_in_background: true
```
### Database migrations
```bash
# Alembic (Python)
alembic upgrade head # run_in_background: true for large migrations
# Prisma (TypeScript)
npx prisma migrate deploy # run_in_background: true
# Verify migration status
alembic check # Python
npx prisma migrate status # TypeScript
```
---
## Anti-Patterns
### Don't: Sleep loops
```bash
# BAD — burns cache, wastes tokens
sleep 60 && check_status
sleep 60 && check_status
sleep 60 && check_status
# GOOD — use run_in_background or until-loop with Monitor
```
### Don't: Poll too frequently
```bash
# BAD — checking every second
while true; do curl localhost:3000/health; sleep 1; done
# GOOD — reasonable interval based on expected duration
until curl -sf localhost:3000/health; do sleep 5; done
```
### Don't: Wait without timeouts
```bash
# BAD — waits forever
until curl -sf localhost:3000/health; do sleep 5; done
# GOOD — timeout after 5 minutes
timeout 300 bash -c 'until curl -sf localhost:3000/health; do sleep 5; done'
```
### Don't: Guess completion
```markdown
BAD: "The build probably finished by now, let's proceed"
GOOD: "Let me check the build status before proceeding"
```
---
## Timing Guide
| Operation | Expected Duration | Check Interval | Approach |
|-----------|------------------|----------------|----------|
| Unit tests (small) | 5-30s | N/A | Run inline |
| Unit tests (large) | 30s-5m | N/A | `run_in_background` |
| `next build` | 30s-3m | N/A | `run_in_background` |
| Docker build | 1-10m | N/A | `run_in_background` |
| CI pipeline | 2-15m | 30s | `gh run watch` |
| Deployment | 1-10m | 5s | Health check poll |
| DB migration (small) | 5-30s | N/A | Run inline |
| DB migration (large) | 1-30m | N/A | `run_in_background` |
---
## Related Skills
- `verification-before-completion` — After waiting, verify the result before claiming success
- `github-actions` — CI/CD workflow patterns
- `docker` — Container build patterns
- `systematic-debugging` — When the thing you're waiting for fails
+64
View File
@@ -0,0 +1,64 @@
---
name: databases
description: >
Use when working with PostgreSQL, MongoDB, or Redis — including schema design, queries, indexing, migrations, connection pooling, caching layers, or any database operation. Also activate for keywords like SQL, aggregation pipeline, BSON, ioredis, alembic, prisma migrate, django migrate, EXPLAIN ANALYZE, ORM configuration, or NoSQL data modeling.
---
# Databases
## When to Use
- PostgreSQL database operations, SQL query optimization, schema design
- JSONB document storage, full-text search, window functions, CTEs
- MongoDB document modeling, aggregation pipelines, semi-structured data
- Redis caching, session storage, rate limiting, pub/sub, job queues, distributed locks
- Database migrations — adding/modifying tables, columns, indexes, constraints
- Resolving migration conflicts, rolling back failed migrations
## When NOT to Use
- Simple key-value caching within a single process — use `functools.lru_cache` or `Map`
- File-based storage that doesn't need a database engine
- Static data or configuration that belongs in environment variables
---
## Quick Reference
| Topic | Reference | Key tools |
|-------|-----------|-----------|
| PostgreSQL | `references/postgresql.md` | SQL, SQLAlchemy, Prisma, EXPLAIN ANALYZE, pg_stat_statements |
| MongoDB | `references/mongodb.md` | Aggregation, Mongoose, Motor, document schemas, ESR indexing |
| Redis | `references/redis.md` | Caching, pub/sub, ioredis, BullMQ, session storage, distributed locks |
| Migrations | `references/migrations.md` | Alembic, Prisma Migrate, Django migrations, rollback strategies |
---
## Best Practices
1. **Use parameterized queries everywhere.** Never concatenate user input into SQL strings.
2. **Design schema around access patterns.** Ask "how will I read this?" before "how does this relate?" Embed data fetched together (MongoDB); normalize data accessed independently (PostgreSQL).
3. **Index foreign keys and query fields.** PostgreSQL doesn't auto-index FK child columns. MongoDB queries without indexes trigger full collection scans.
4. **Use appropriate consistency levels.** `TIMESTAMPTZ` over `TIMESTAMP` (PostgreSQL). `w: "majority"` for durable writes (MongoDB). TTLs on every Redis cache key.
5. **Monitor query performance.** `pg_stat_statements` (PostgreSQL), `db.setProfilingLevel(1)` (MongoDB), connection pool metrics (all).
6. **Use bulk/batch operations.** `bulkWrite` (MongoDB), `COPY` (PostgreSQL), pipelines (Redis) for high-throughput writes.
7. **Never edit deployed migrations.** Create a new migration instead of modifying one already applied.
8. **Test rollback paths.** Always verify your downgrade/rollback strategy before deploying schema changes.
## Common Pitfalls
1. **N+1 queries from ORM lazy loading.** Use eager loading (`joinedload`, `select_related`, `$lookup` with caution).
2. **Table locks during migrations.** Use `CREATE INDEX CONCURRENTLY` (PostgreSQL). Batch backfills for large tables.
3. **Unbounded growth.** Dead tuples from UPDATE-heavy workloads (PostgreSQL). Arrays exceeding 16MB document limit (MongoDB). Redis keys without TTLs.
4. **OFFSET pagination on large datasets.** Use keyset/cursor pagination instead.
5. **Connection exhaustion.** Use connection pools (PgBouncer, application-level pools). Never open per-request connections.
6. **Cache stampede.** When a popular Redis key expires, many requests hit the DB simultaneously. Use distributed locks or stale-while-revalidate.
7. **Running `migrate reset` in production.** This drops all data.
---
## Related Skills
- `backend-frameworks` — Framework-specific ORM integration
- `error-handling` — Database error handling patterns
- `logging` — Query logging and slow query detection
@@ -1,237 +0,0 @@
# MongoDB Schema Design Patterns
Quick reference for embedding vs referencing decisions and common schema patterns.
## Embedding vs Referencing Decision Tree
```
What is the relationship cardinality?
|
+-- One-to-Few (< 50 items)?
| --> EMBED in parent document
| Example: user.addresses, post.tags
|
+-- One-to-Many (50 - 1000s)?
| |
| +-- Child data always accessed with parent?
| | --> EMBED (but watch 16 MB doc limit)
| |
| +-- Child data accessed independently?
| | --> REFERENCE (store child _id in parent array)
| |
| +-- Need atomic updates across parent + children?
| --> EMBED
|
+-- One-to-Millions?
| --> REFERENCE from child to parent
| Example: log_entry.host_id (not host.log_entry_ids)
|
+-- Many-to-Many?
--> REFERENCE with array of _ids on one or both sides
Example: student.course_ids[], course.student_ids[]
```
## Decision Factors
| Factor | Favor Embedding | Favor Referencing |
|--------|----------------|-------------------|
| **Read pattern** | Always read together | Read independently |
| **Write pattern** | Infrequent child updates | Frequent child updates |
| **Data size** | Small, bounded children | Large or growing children |
| **Atomicity** | Need single-doc transactions | Can tolerate multi-doc txn |
| **Duplication** | OK to denormalize | Must avoid duplication |
| **Cardinality** | Few items | Many/unbounded items |
| **Document size** | Well under 16 MB limit | Approaching 16 MB |
## Pattern Catalog
### 1. Subset Pattern
**Problem**: Document is large but reads only need a few fields from embedded data.
**Solution**: Embed a subset; keep full data in a separate collection.
```javascript
// products collection - fast reads for listing pages
{
_id: ObjectId("..."),
name: "Widget",
price: 29.99,
// Only the 10 most recent reviews (subset)
recent_reviews: [
{ user: "alice", rating: 5, text: "Great!", date: ISODate("...") }
],
review_count: 247
}
// reviews collection - full review data
{
_id: ObjectId("..."),
product_id: ObjectId("..."),
user: "alice",
rating: 5,
text: "Great!",
date: ISODate("..."),
helpful_votes: 12
}
```
**When to use**: Product pages, user profiles, any "preview + detail" pattern.
### 2. Computed Pattern
**Problem**: Expensive aggregation queries run repeatedly on the same data.
**Solution**: Pre-compute and store the result, update on write.
```javascript
// movies collection
{
_id: ObjectId("..."),
title: "Example Movie",
// Pre-computed from screenings collection
computed: {
total_revenue: 1250000,
avg_rating: 4.2,
rating_count: 843,
last_computed: ISODate("2025-01-15T00:00:00Z")
}
}
```
**Update strategy**: On each new rating, increment count and recalculate average. Or use a background job for less time-sensitive data.
**When to use**: Dashboards, leaderboards, summary statistics.
### 3. Bucket Pattern
**Problem**: Many small, time-series documents create overhead (indexes, storage per doc).
**Solution**: Group related data into fixed-size buckets.
```javascript
// sensor_readings collection - one doc per sensor per hour
{
sensor_id: "sensor-42",
bucket_start: ISODate("2025-01-15T14:00:00Z"),
bucket_end: ISODate("2025-01-15T14:59:59Z"),
count: 60,
readings: [
{ ts: ISODate("2025-01-15T14:00:00Z"), temp: 22.1, humidity: 45 },
{ ts: ISODate("2025-01-15T14:01:00Z"), temp: 22.3, humidity: 44 }
// ... up to 60 readings per bucket
],
// Pre-computed aggregates for the bucket
summary: {
avg_temp: 22.4,
min_temp: 21.8,
max_temp: 23.1
}
}
```
**Bucket sizing**: Choose a size that balances doc count reduction vs update frequency. Common choices: 1 hour, 1 day, 100 events.
**When to use**: IoT, time-series, event logging, analytics.
### 4. Outlier Pattern
**Problem**: A few documents have vastly more data than the norm (e.g., a viral post with millions of likes).
**Solution**: Flag outliers and overflow into separate documents.
```javascript
// books collection - normal case
{
_id: ObjectId("..."),
title: "Normal Book",
customers_purchased: ["user1", "user2", "user3"],
has_overflow: false
}
// books collection - outlier (bestseller)
{
_id: ObjectId("..."),
title: "Bestseller",
customers_purchased: ["user1", "user2", /* ... first 1000 */],
has_overflow: true
}
// book_purchases_overflow collection
{
book_id: ObjectId("..."),
page: 2,
customers_purchased: ["user1001", "user1002", /* ... next 1000 */]
}
```
**When to use**: Social media (viral posts), e-commerce (bestsellers), any data with power-law distribution.
### 5. Extended Reference Pattern
**Problem**: Frequent joins (lookups) to get a few fields from a referenced document.
**Solution**: Copy the most-accessed fields into the referencing document.
```javascript
// orders collection
{
_id: ObjectId("..."),
date: ISODate("..."),
customer_id: ObjectId("..."),
// Extended reference - copied fields for fast reads
customer_name: "Alice Smith",
customer_email: "alice@example.com",
items: [
{
product_id: ObjectId("..."),
product_name: "Widget", // copied
price: 29.99, // copied (snapshot at time of order)
quantity: 2
}
]
}
```
**Trade-off**: Stale data is acceptable (order snapshots price at purchase time). For data that must be current, keep only the reference.
**When to use**: Orders (snapshot pricing), notifications (snapshot user name), audit logs.
### 6. Polymorphic Pattern
**Problem**: Objects share some fields but differ in others (e.g., different product types).
**Solution**: Store in a single collection with a type discriminator.
```javascript
// vehicles collection
{ type: "car", make: "Toyota", doors: 4, trunk_size_liters: 450 }
{ type: "truck", make: "Ford", doors: 2, payload_kg: 5000 }
{ type: "motorcycle", make: "Harley", engine_cc: 1200 }
```
**Index strategy**: Index common fields. Use partial indexes for type-specific fields.
```javascript
db.vehicles.createIndex(
{ payload_kg: 1 },
{ partialFilterExpression: { type: "truck" } }
);
```
**When to use**: Product catalogs, content management (articles, videos, images), mixed event streams.
## Anti-Patterns
| Mistake | Problem | Fix |
|---------|---------|-----|
| Unbounded array growth | Document exceeds 16 MB | Use bucket or outlier pattern |
| Deep nesting (> 3 levels) | Hard to query and index | Flatten or reference |
| Normalizing everything | Too many lookups, slow reads | Embed when read together |
| Embedding large blobs | Wastes RAM in working set | Store in GridFS or S3 |
| No schema validation | Inconsistent data over time | Use JSON Schema validation |
| Indexing every field | Slow writes, wasted space | Index based on query patterns |
## Schema Validation
Use `db.createCollection()` with `$jsonSchema` validator to enforce structure. Set `validationLevel: "moderate"` to apply only on inserts and updates (not existing docs).
@@ -1,173 +0,0 @@
# PostgreSQL Index Decision Tree
Quick reference for choosing the right index type.
## Decision Tree
```
What are you querying?
|
+-- Equality (=) or Range (<, >, BETWEEN, ORDER BY)?
| |
| +-- On a single scalar column?
| | --> B-tree (default)
| |
| +-- On a timestamp/date column with append-only inserts?
| | --> BRIN (much smaller than B-tree)
| |
| +-- Need the index to also return columns without table lookup?
| --> Covering Index (B-tree with INCLUDE)
|
+-- Array containment (@>, &&) or JSONB queries?
| --> GIN
|
+-- Full-text search (tsvector, @@)?
| --> GIN
|
+-- Geometric/spatial data (points, polygons, PostGIS)?
| --> GiST
|
+-- Range types (int4range, tsrange, overlaps)?
| --> GiST
|
+-- Nearest-neighbor / distance queries (KNN)?
| --> GiST (or SP-GiST for partitioned space)
|
+-- Only a subset of rows match your WHERE clause?
| --> Partial Index (any type + WHERE filter)
|
+-- Trigram similarity (LIKE '%pattern%', pg_trgm)?
| --> GIN with pg_trgm (or GiST for smaller, slower)
|
+-- Hash equality only (= but never range)?
--> Hash index (rarely better than B-tree in practice)
```
## Index Type Comparison
| Type | Best For | Operators | Size | Write Cost | Notes |
|------|----------|-----------|------|------------|-------|
| **B-tree** | Equality, range, sorting | `= < > <= >= BETWEEN IN IS NULL` | Medium | Low | Default. Covers 90% of cases. |
| **GIN** | Multi-valued data | `@> && @@ ? ?& ?|` | Large | High (slow updates) | Best for arrays, JSONB, full-text. Use `fastupdate=on`. |
| **GiST** | Spatial, ranges, nearest-neighbor | `<< >> && @> <@ <->` | Medium | Medium | Lossy for some types. Supports KNN. |
| **SP-GiST** | Partitioned search spaces | Same as GiST | Medium | Medium | Good for phone numbers, IP addresses, non-balanced trees. |
| **BRIN** | Large sequential/append-only tables | `= < > <= >=` | Tiny | Very Low | 1000x smaller than B-tree. Only effective when physical order correlates with column values. |
| **Hash** | Equality only | `=` | Medium | Low | WAL-logged since PG10. Rarely outperforms B-tree. |
## Common Patterns
### Covering Index (Index-Only Scans)
Avoid heap lookups by including extra columns:
```sql
-- Query: SELECT email, name FROM users WHERE email = ?
CREATE INDEX idx_users_email_covering
ON users (email) INCLUDE (name);
```
### Partial Index (Filtered)
Index only the rows you actually query:
```sql
-- Only index active orders (skip 95% of rows)
CREATE INDEX idx_orders_active
ON orders (created_at)
WHERE status = 'active';
```
### Composite Index (Multi-Column)
Column order matters -- put equality columns first, range columns last:
```sql
-- Query: WHERE tenant_id = ? AND created_at > ?
CREATE INDEX idx_events_tenant_date
ON events (tenant_id, created_at);
```
### Expression Index
Index a computed value:
```sql
CREATE INDEX idx_users_lower_email
ON users (lower(email));
```
### GIN for JSONB
```sql
-- Index all keys and values in a JSONB column
CREATE INDEX idx_metadata_gin
ON products USING gin (metadata jsonb_path_ops);
-- Supports: metadata @> '{"color": "red"}'
```
### GiST for Range Overlap
```sql
CREATE INDEX idx_reservations_during
ON reservations USING gist (during);
-- Supports: WHERE during && '[2025-01-01, 2025-01-31]'::daterange
```
### BRIN for Time-Series
```sql
-- Table has millions of rows inserted in timestamp order
CREATE INDEX idx_logs_ts_brin
ON logs USING brin (created_at)
WITH (pages_per_range = 32);
```
## Sizing Rules of Thumb
| Table Rows | B-tree Size | BRIN Size | GIN Size |
|------------|-------------|-----------|----------|
| 1M | ~20 MB | ~50 KB | ~30 MB |
| 10M | ~200 MB | ~500 KB | ~300 MB |
| 100M | ~2 GB | ~5 MB | ~3 GB |
## Diagnostic Queries
```sql
-- Check if an index is being used
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;
-- Find unused indexes
SELECT indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
-- Check index size
SELECT pg_size_pretty(pg_relation_size('idx_name'));
-- Index bloat estimate
SELECT * FROM pgstatindex('idx_name');
```
## Anti-Patterns
| Mistake | Why It Hurts |
|---------|-------------|
| Indexing every column | Slows writes, wastes disk, confuses planner |
| Wrong column order in composite | Index cannot be used for the query |
| GIN on tiny tables | Overhead exceeds benefit |
| B-tree on low-cardinality columns | Planner prefers seq scan anyway |
| Missing `CONCURRENTLY` on production | Locks the table during index build |
| Forgetting `ANALYZE` after bulk load | Planner uses stale statistics |
## Safe Index Creation
```sql
-- Non-blocking index creation (no table lock)
CREATE INDEX CONCURRENTLY idx_name ON table (column);
-- Always run ANALYZE after bulk operations
ANALYZE table;
```
@@ -1,143 +0,0 @@
-- =============================================================================
-- Migration: [DESCRIPTION]
-- Created: [DATE]
-- Author: [AUTHOR]
-- Ticket: [TICKET-ID]
-- =============================================================================
--
-- SAFETY CHECKLIST (review before running):
-- [ ] Tested on staging with production-size data
-- [ ] Backward compatible with current application code
-- [ ] No exclusive locks on large tables during peak hours
-- [ ] Rollback (DOWN) section tested independently
-- [ ] Estimated run time: ___
-- [ ] Estimated lock duration: ___
--
-- ============================================================
-- UP MIGRATION
-- ============================================================
BEGIN;
-- Set a statement timeout to prevent long-running locks.
-- Adjust as needed; remove for data-only migrations.
SET LOCAL lock_timeout = '5s';
SET LOCAL statement_timeout = '30s';
-- ------------------------------------
-- 1. Schema changes
-- ------------------------------------
-- Add new table
-- CREATE TABLE IF NOT EXISTS example (
-- id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
-- name text NOT NULL,
-- created_at timestamptz NOT NULL DEFAULT now(),
-- updated_at timestamptz NOT NULL DEFAULT now()
-- );
-- Add column (safe: does not rewrite table)
-- ALTER TABLE example ADD COLUMN IF NOT EXISTS description text;
-- Add column with default (PG 11+: does not rewrite table)
-- ALTER TABLE example ADD COLUMN IF NOT EXISTS is_active boolean NOT NULL DEFAULT true;
-- Rename column (safe: metadata-only change)
-- ALTER TABLE example RENAME COLUMN old_name TO new_name;
-- ------------------------------------
-- 2. Constraints
-- ------------------------------------
-- Add NOT NULL (requires all existing rows to satisfy it)
-- ALTER TABLE example ALTER COLUMN name SET NOT NULL;
-- Add check constraint (NOT VALID avoids full table scan, then VALIDATE separately)
-- ALTER TABLE example ADD CONSTRAINT chk_example_name CHECK (name <> '') NOT VALID;
-- ALTER TABLE example VALIDATE CONSTRAINT chk_example_name;
-- Add foreign key (NOT VALID + VALIDATE pattern to avoid long locks)
-- ALTER TABLE example ADD CONSTRAINT fk_example_parent
-- FOREIGN KEY (parent_id) REFERENCES parent(id) NOT VALID;
-- ALTER TABLE example VALIDATE CONSTRAINT fk_example_parent;
-- ------------------------------------
-- 3. Indexes (use CONCURRENTLY outside transaction)
-- ------------------------------------
-- NOTE: CREATE INDEX CONCURRENTLY cannot run inside a transaction.
-- Run these statements separately after committing the transaction above.
--
-- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_example_name
-- ON example (name);
--
-- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_example_created_at
-- ON example USING brin (created_at);
-- ------------------------------------
-- 4. Data migration
-- ------------------------------------
-- Backfill in batches to avoid long transactions:
-- UPDATE example SET description = 'default' WHERE description IS NULL;
--
-- For large tables, batch with:
-- DO $$
-- DECLARE
-- batch_size int := 10000;
-- rows_updated int;
-- BEGIN
-- LOOP
-- UPDATE example
-- SET description = 'default'
-- WHERE id IN (
-- SELECT id FROM example
-- WHERE description IS NULL
-- LIMIT batch_size
-- FOR UPDATE SKIP LOCKED
-- );
-- GET DIAGNOSTICS rows_updated = ROW_COUNT;
-- EXIT WHEN rows_updated = 0;
-- RAISE NOTICE 'Updated % rows', rows_updated;
-- COMMIT;
-- END LOOP;
-- END $$;
-- ------------------------------------
-- 5. Permissions
-- ------------------------------------
-- GRANT SELECT, INSERT, UPDATE ON example TO app_role;
-- GRANT USAGE ON SEQUENCE example_id_seq TO app_role;
COMMIT;
-- ============================================================
-- DOWN MIGRATION (rollback)
-- ============================================================
-- Run this section to undo the UP migration.
-- Test this independently before deploying the UP migration.
-- BEGIN;
--
-- -- Reverse data migration
-- -- UPDATE example SET description = NULL;
--
-- -- Drop constraints
-- -- ALTER TABLE example DROP CONSTRAINT IF EXISTS chk_example_name;
-- -- ALTER TABLE example DROP CONSTRAINT IF EXISTS fk_example_parent;
--
-- -- Drop columns
-- -- ALTER TABLE example DROP COLUMN IF EXISTS description;
-- -- ALTER TABLE example DROP COLUMN IF EXISTS is_active;
--
-- -- Drop tables
-- -- DROP TABLE IF EXISTS example;
--
-- COMMIT;
--
-- -- Drop indexes (outside transaction)
-- -- DROP INDEX CONCURRENTLY IF EXISTS idx_example_name;
-- -- DROP INDEX CONCURRENTLY IF EXISTS idx_example_created_at;
@@ -0,0 +1,312 @@
# Databases — Migration Patterns
# Database Migrations
## When to Use
- Adding or modifying database tables/columns
- Creating indexes or constraints
- Running migrations in development, staging, or production
- Resolving migration conflicts in a team
- Rolling back a failed migration
## When NOT to Use
- Query optimization without schema changes — use `postgresql` skill
- Initial database design from scratch — use `postgresql` or `mongodb` skill
- ORM configuration without migrations — use framework-specific skill
---
## Quick Reference
| I need... | Go to |
|-----------|-------|
| Alembic (FastAPI/SQLAlchemy) | SS Alembic below |
| Prisma (NestJS/Express) | SS Prisma below |
| Django migrations | SS Django below |
| Safe production patterns | SS Production Safety below |
| Rollback strategies | SS Rollbacks below |
---
## Alembic (Python / SQLAlchemy)
### Setup
```bash
pip install alembic
alembic init migrations
```
```python
# migrations/env.py — configure target metadata
from src.models import Base
target_metadata = Base.metadata
```
### Create a migration
```bash
# Auto-generate from model changes
alembic revision --autogenerate -m "add orders table"
# Manual migration (for data migrations or complex changes)
alembic revision -m "backfill order status"
```
### Migration file
```python
# migrations/versions/003_add_orders_table.py
"""add orders table"""
from alembic import op
import sqlalchemy as sa
revision = '003'
down_revision = '002'
def upgrade() -> None:
op.create_table(
'orders',
sa.Column('id', sa.UUID(), primary_key=True, server_default=sa.text('gen_random_uuid()')),
sa.Column('user_id', sa.UUID(), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False),
sa.Column('total', sa.Numeric(10, 2), nullable=False),
sa.Column('status', sa.String(20), nullable=False, server_default='pending'),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index('ix_orders_user_id', 'orders', ['user_id'])
op.create_index('ix_orders_created_at', 'orders', ['created_at'])
def downgrade() -> None:
op.drop_table('orders')
```
### Run migrations
```bash
# Apply all pending
alembic upgrade head
# Apply one step
alembic upgrade +1
# Check current state
alembic current
# Check for pending migrations
alembic check
# View migration history
alembic history --verbose
```
---
## Prisma (TypeScript / NestJS / Express)
### Create a migration
```bash
# Generate migration from schema changes
npx prisma migrate dev --name add_orders_table
# Apply in production (no interactive prompts)
npx prisma migrate deploy
# Check status
npx prisma migrate status
```
### Schema change
```prisma
// prisma/schema.prisma
model Order {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
total Decimal @db.Decimal(10, 2)
status String @default("pending")
createdAt DateTime @default(now())
@@index([userId])
@@index([createdAt])
}
```
### Generated migration SQL
```sql
-- prisma/migrations/20260417_add_orders_table/migration.sql
CREATE TABLE "Order" (
"id" TEXT NOT NULL DEFAULT gen_random_uuid(),
"userId" TEXT NOT NULL,
"total" DECIMAL(10,2) NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Order_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "Order_userId_idx" ON "Order"("userId");
CREATE INDEX "Order_createdAt_idx" ON "Order"("createdAt");
ALTER TABLE "Order" ADD CONSTRAINT "Order_userId_fkey"
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE;
```
---
## Django
### Create and apply
```bash
# Auto-generate from model changes
python manage.py makemigrations app_name
# Apply
python manage.py migrate
# Check for pending
python manage.py showmigrations
# SQL preview (don't execute)
python manage.py sqlmigrate app_name 0003
```
### Data migration
```python
# app/migrations/0004_backfill_order_status.py
from django.db import migrations
def backfill_status(apps, schema_editor):
Order = apps.get_model('orders', 'Order')
Order.objects.filter(status='').update(status='pending')
class Migration(migrations.Migration):
dependencies = [('orders', '0003_add_orders')]
operations = [migrations.RunPython(backfill_status, migrations.RunPython.noop)]
```
---
## Production Safety
### Golden rules
1. **Never drop columns in the same deploy as removing code references.** Remove code first, deploy, then drop column in next migration.
2. **Add columns as nullable or with defaults.** `NOT NULL` without a default locks the table during backfill on large tables.
3. **Create indexes concurrently** (PostgreSQL):
```sql
CREATE INDEX CONCURRENTLY ix_orders_status ON orders(status);
```
4. **Test migrations against a production-size dataset** before deploying.
5. **Always have a rollback plan** — either a `downgrade()` function or a manual SQL script.
### Safe column addition pattern
```python
# Step 1: Add nullable column (fast, no lock)
op.add_column('users', sa.Column('phone', sa.String(20), nullable=True))
# Step 2: Backfill in batches (separate migration or script)
# Don't do UPDATE users SET phone = '...' on millions of rows at once
# Step 3: Add NOT NULL constraint (after backfill confirms all rows filled)
op.alter_column('users', 'phone', nullable=False)
```
### Safe column rename pattern
```
Deploy 1: Add new column, write to both old and new
Deploy 2: Backfill new column from old, read from new
Deploy 3: Stop writing to old column
Deploy 4: Drop old column
```
---
## Rollbacks
### Alembic
```bash
# Rollback one step
alembic downgrade -1
# Rollback to specific revision
alembic downgrade 002
# Rollback to base (dangerous — drops everything)
alembic downgrade base
```
### Prisma
Prisma doesn't have built-in rollback. Options:
- Apply a new migration that reverses the change
- Manually run SQL: `npx prisma db execute --file rollback.sql`
- Restore from database backup
### Django
```bash
# Rollback to specific migration
python manage.py migrate app_name 0002
```
---
## Team Workflow
### Resolving migration conflicts
When two developers create migrations from the same parent:
**Alembic:**
```bash
# Developer A and B both branched from revision 002
# Alembic detects multiple heads
alembic heads # shows 003a and 003b
alembic merge -m "merge migrations" 003a 003b
alembic upgrade head
```
**Prisma:**
```bash
# Reset and re-apply (dev only)
npx prisma migrate reset
# Or resolve manually by editing the migration SQL
```
**Django:**
```bash
# Django auto-detects and asks to merge
python manage.py makemigrations --merge
```
---
## Common Pitfalls
1. **Running `migrate reset` in production.** This drops all data. Only use in development.
2. **Editing already-applied migrations.** Never modify a migration that's been deployed. Create a new migration instead.
3. **Forgetting indexes.** Add indexes for foreign keys and frequently-queried columns in the same migration.
4. **Large table locks.** `ALTER TABLE` with `NOT NULL` or `ADD COLUMN DEFAULT` can lock large tables. Use batched backfills.
5. **Not testing downgrade.** Always test your rollback path before deploying.
6. **Circular foreign keys.** Use `sa.ForeignKey` with `use_alter=True` in Alembic to handle circular deps.
---
## Related Skills
- `postgresql` — Database design, query optimization, indexing strategies
- `fastapi` — SQLAlchemy async patterns with FastAPI
- `nestjs` — Prisma integration with NestJS
- `django` — Django ORM models and migrations
- `docker` — Running migration containers in CI/CD
@@ -1,8 +1,5 @@
---
name: mongodb
description: >
Use this skill whenever working with MongoDB, document databases, or NoSQL data modeling. Trigger on keywords like MongoDB, Mongo, document database, aggregation pipeline, collection, embedded documents, or BSON. Also applies when designing document schemas, building aggregation queries, handling unstructured or semi-structured data, or migrating from relational to document-based storage.
---
# Databases — MongoDB Patterns
# MongoDB
@@ -574,6 +571,6 @@ db.orders.updateMany(
## Related Skills
- `databases/postgresql` - Relational database patterns for structured data with complex relationships
- `patterns/caching` - Caching strategies to reduce database load
- `patterns/logging` - Logging patterns for query debugging and monitoring
- `postgresql` - Relational database patterns for structured data with complex relationships
- `caching` - Caching strategies to reduce database load
- `logging` - Logging patterns for query debugging and monitoring
@@ -1,8 +1,5 @@
---
name: postgresql
description: >
Use this skill whenever working with PostgreSQL databases, writing SQL queries, designing schemas, or optimizing database performance. Trigger on keywords like PostgreSQL, Postgres, SQL query, schema design, indexing, migrations, EXPLAIN ANALYZE, connection pooling, or any relational database operation. Also applies when debugging slow queries, setting up database tables, or working with ORMs that target PostgreSQL.
---
# Databases — PostgreSQL Patterns
# PostgreSQL
@@ -607,6 +604,6 @@ SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20;
## Related Skills
- `databases/mongodb` - Document-based database patterns for non-relational data
- `patterns/caching` - Caching strategies to reduce database load
- `patterns/logging` - Logging patterns for query debugging and monitoring
- `mongodb` - Document-based database patterns for non-relational data
- `caching` - Caching strategies to reduce database load
- `logging` - Logging patterns for query debugging and monitoring
@@ -0,0 +1,279 @@
# Databases — Redis Patterns
# Redis
## When to Use
- Caching database queries or API responses
- Session storage for web applications
- Rate limiting (distributed across instances)
- Job/task queues (BullMQ, Celery)
- Pub/sub messaging between services
- Distributed locks
## When NOT to Use
- **Primary data storage** — Redis is a cache/broker, not a database of record
- **Complex queries** — use PostgreSQL for relational queries
- **Large blobs** — use S3/R2 for file storage
- **In-memory caching only** — use `functools.lru_cache` or `Map` for single-process caches
---
## Python (redis-py / FastAPI)
### Connection
```python
# src/core/redis.py
import redis.asyncio as redis
pool = redis.ConnectionPool.from_url(
"redis://localhost:6379/0",
max_connections=20,
decode_responses=True,
)
async def get_redis() -> redis.Redis:
return redis.Redis(connection_pool=pool)
```
### Cache-aside pattern
```python
import json
from datetime import timedelta
async def get_user_cached(user_id: str, db: AsyncSession) -> User:
r = await get_redis()
cache_key = f"user:{user_id}"
# Check cache
cached = await r.get(cache_key)
if cached:
return User(**json.loads(cached))
# Cache miss — fetch from DB
user = await db.get(User, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Store in cache with TTL
await r.setex(cache_key, timedelta(minutes=15), json.dumps(user.to_dict()))
return user
```
### Cache invalidation
```python
async def update_user(user_id: str, data: UpdateUserRequest, db: AsyncSession) -> User:
user = await db.get(User, user_id)
for key, value in data.dict(exclude_unset=True).items():
setattr(user, key, value)
await db.commit()
# Invalidate cache
r = await get_redis()
await r.delete(f"user:{user_id}")
return user
```
### Rate limiting
```python
from fastapi import Request, HTTPException
async def rate_limit(request: Request, limit: int = 100, window: int = 900):
r = await get_redis()
key = f"rate:{request.client.host}"
current = await r.incr(key)
if current == 1:
await r.expire(key, window)
if current > limit:
raise HTTPException(status_code=429, detail="Rate limit exceeded")
```
### Session storage
```python
import secrets
async def create_session(user_id: str) -> str:
r = await get_redis()
session_id = secrets.token_urlsafe(32)
await r.setex(f"session:{session_id}", timedelta(hours=24), user_id)
return session_id
async def get_session(session_id: str) -> str | None:
r = await get_redis()
return await r.get(f"session:{session_id}")
async def delete_session(session_id: str):
r = await get_redis()
await r.delete(f"session:{session_id}")
```
---
## TypeScript (ioredis / NestJS / Express)
### Connection
```typescript
// src/core/redis.ts
import Redis from 'ioredis';
export const redis = new Redis(process.env.REDIS_URL ?? 'redis://localhost:6379', {
maxRetriesPerRequest: 3,
lazyConnect: true,
});
```
### NestJS module
```typescript
// src/cache/cache.module.ts
import { Global, Module } from '@nestjs/common';
import { CacheService } from './cache.service';
@Global()
@Module({
providers: [CacheService],
exports: [CacheService],
})
export class CacheModule {}
```
```typescript
// src/cache/cache.service.ts
import { Injectable, OnModuleDestroy } from '@nestjs/common';
import Redis from 'ioredis';
@Injectable()
export class CacheService implements OnModuleDestroy {
private readonly redis = new Redis(process.env.REDIS_URL!);
async get<T>(key: string): Promise<T | null> {
const data = await this.redis.get(key);
return data ? JSON.parse(data) : null;
}
async set(key: string, value: unknown, ttlSeconds: number): Promise<void> {
await this.redis.setex(key, ttlSeconds, JSON.stringify(value));
}
async del(key: string): Promise<void> {
await this.redis.del(key);
}
async onModuleDestroy() {
await this.redis.quit();
}
}
```
### Cache-aside in service
```typescript
@Injectable()
export class UsersService {
constructor(
private readonly prisma: PrismaService,
private readonly cache: CacheService,
) {}
async findOne(id: string): Promise<User> {
// Check cache
const cached = await this.cache.get<User>(`user:${id}`);
if (cached) return cached;
// Cache miss
const user = await this.prisma.user.findUnique({ where: { id } });
if (!user) throw new NotFoundException(`User ${id} not found`);
// Store with 15min TTL
await this.cache.set(`user:${id}`, user, 900);
return user;
}
async update(id: string, dto: UpdateUserDto): Promise<User> {
const user = await this.prisma.user.update({ where: { id }, data: dto });
await this.cache.del(`user:${id}`); // Invalidate
return user;
}
}
```
---
## Pub/Sub
### Python
```python
# Publisher
async def publish_event(channel: str, event: dict):
r = await get_redis()
await r.publish(channel, json.dumps(event))
# Subscriber
async def subscribe_events(channel: str):
r = await get_redis()
pubsub = r.pubsub()
await pubsub.subscribe(channel)
async for message in pubsub.listen():
if message['type'] == 'message':
yield json.loads(message['data'])
```
### TypeScript
```typescript
// Publisher
const pub = new Redis(process.env.REDIS_URL!);
await pub.publish('orders', JSON.stringify({ type: 'created', orderId: '123' }));
// Subscriber (separate connection required)
const sub = new Redis(process.env.REDIS_URL!);
sub.subscribe('orders');
sub.on('message', (channel, message) => {
const event = JSON.parse(message);
console.log(`[${channel}]`, event);
});
```
---
## Key Naming Conventions
```
entity:id → user:abc123
entity:id:field → user:abc123:orders
rate:ip → rate:192.168.1.1
session:token → session:abc123def
lock:resource → lock:order-processing
queue:name → queue:email-notifications
```
---
## Common Pitfalls
1. **Not setting TTLs.** Every cache key should have an expiration. Unbounded caches exhaust memory.
2. **Cache stampede.** When a popular key expires, many requests hit the DB simultaneously. Use distributed locks or stale-while-revalidate.
3. **Using the same connection for pub/sub.** Subscribers can't run other commands. Use a dedicated connection.
4. **Storing large objects.** Redis is fast for small values. Keep values under 1MB; for larger data, store a pointer to S3.
5. **Not handling connection failures.** Redis connections drop. Use retry logic and connection pools.
6. **Forgetting to invalidate.** When data changes, delete the cache key. Stale cache is worse than no cache.
---
## Related Skills
- `caching` — HTTP caching, CDN, memoization (framework-agnostic patterns)
- `background-jobs` — BullMQ/Celery use Redis as broker
- `fastapi` — Redis integration with FastAPI dependency injection
- `nestjs` — Redis caching module in NestJS
- `docker` — Running Redis in Docker Compose for development
@@ -1,7 +1,7 @@
---
name: defense-in-depth
description: >
Trigger this skill after fixing any data-related bug, when building validation for critical data paths, or when a single validation point has already failed in production. Use whenever you hear "it slipped through," "the check was bypassed," or "it worked in tests but not production." Apply aggressively to any scenario involving data integrity, input validation across layers, or preventing bug recurrence through structural guarantees rather than single-point fixes.
Use when fixing any data-related bug, when building validation for critical data paths, or when a single validation point has already failed in production. Also activate whenever you hear "it slipped through," "the check was bypassed," or "it worked in tests but not production." Apply aggressively to any scenario involving data integrity, input validation across layers, or preventing bug recurrence through structural guarantees rather than single-point fixes.
---
# Defense-in-Depth
@@ -294,6 +294,6 @@ After fixing any bug:
## Related Skills
- `methodology/root-cause-tracing` - Use before defense-in-depth to find the actual source of the bug before adding multi-layer validation
- `methodology/systematic-debugging` - General debugging methodology that pairs with defense-in-depth for comprehensive bug resolution
- `security/owasp` - Security-specific validation patterns that complement defense-in-depth for security-sensitive code paths
- `root-cause-tracing` - Use before defense-in-depth to find the actual source of the bug before adding multi-layer validation
- `systematic-debugging` - General debugging methodology that pairs with defense-in-depth for comprehensive bug resolution
- `owasp` - Security-specific validation patterns that complement defense-in-depth for security-sensitive code paths
+64
View File
@@ -0,0 +1,64 @@
---
name: devops
description: >
Use when containerizing applications, configuring CI/CD pipelines, or deploying to edge — including Docker, Dockerfile, docker-compose, multi-stage builds, GitHub Actions, workflow YAML, matrix builds, workflow_dispatch, Cloudflare Workers, Pages, R2, D1, KV, wrangler, or container registries.
---
# DevOps
## When to Use
- Containerizing applications with Docker or Docker Compose
- Setting up CI/CD pipelines with GitHub Actions
- Deploying to Cloudflare Workers, Pages, R2, D1, or KV
- Optimizing container images, build caching, or deployment workflows
- Configuring wrangler.toml, Durable Objects, or Cloudflare Queues
## When NOT to Use
- Application code without infrastructure concerns — use framework-specific skills
- Database schema changes — use `databases`
- Security auditing — use `owasp`
---
## Quick Reference
| Topic | Reference | Key features |
|-------|-----------|-------------|
| Docker | `references/docker.md` | Dockerfiles, multi-stage builds, Compose, .dockerignore, healthchecks |
| GitHub Actions | `references/github-actions.md` | Workflow YAML, matrix builds, caching, secrets, reusable workflows |
| Cloudflare Workers | `references/cloudflare-workers.md` | Workers, Pages, R2, D1, KV, Durable Objects, wrangler |
---
## Best Practices
1. **Use multi-stage builds** to keep production images small (Docker).
2. **Pin image tags and action versions** — use digests or major version tags, never `latest`.
3. **Order instructions for cache efficiency** — copy dependency manifests before application code (Docker).
4. **Run as non-root** in containers (Docker).
5. **Use caching aggressively** in CI — cache package manager stores and Docker layers (GitHub Actions).
6. **Set minimal permissions** — add a top-level `permissions` block (GitHub Actions).
7. **Extract reusable workflows and composite actions** for shared CI logic (GitHub Actions).
8. **Keep secrets out of logs** — never `echo` a secret (GitHub Actions).
## Common Pitfalls
1. **Bloated images** — using full base images instead of slim/alpine variants (Docker).
2. **Cache invalidation by COPY order** — placing `COPY . .` before `RUN pip install` (Docker).
3. **Secrets baked into layers** (Docker).
4. **Unpinned action versions** (GitHub Actions).
5. **Overly broad triggers** — triggering on every push to every branch (GitHub Actions).
6. **Secret exposure in pull requests from forks** (GitHub Actions).
7. **Using Node.js APIs without `nodejs_compat`** (Cloudflare Workers).
8. **Blocking the event loop** — Workers have strict CPU time limits (Cloudflare Workers).
9. **Using KV for frequently updated data** — eventually consistent with ~60s propagation (Cloudflare Workers).
---
## Related Skills
- `backend-frameworks` — Application code that gets containerized
- `databases` — Database services in Docker Compose
- `owasp` — Security hardening for containers and CI
@@ -1,196 +0,0 @@
# Dockerfile Best Practices Reference
Quick reference for writing efficient, secure, and maintainable Dockerfiles.
## Layer Ordering for Cache Optimization
Order instructions from least-frequently-changed to most-frequently-changed:
```dockerfile
# 1. Base image (changes: rarely)
FROM node:22-slim
# 2. System dependencies (changes: rarely)
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
# 3. App dependency manifest (changes: sometimes)
COPY package.json pnpm-lock.yaml ./
# 4. Install dependencies (changes: sometimes, cached if manifests unchanged)
RUN pnpm install --frozen-lockfile
# 5. Copy source code (changes: frequently)
COPY . .
# 6. Build step (changes: frequently)
RUN pnpm build
# 7. Runtime config (changes: rarely, but placed last for clarity)
EXPOSE 3000
CMD ["node", "dist/server.js"]
```
**Key rule**: If a layer changes, all subsequent layers are rebuilt. Separate dependency installation from source code copying.
## Multi-Stage Builds
Reduce final image size by separating build and runtime stages.
```
+-------------------+ +-------------------+
| Build Stage | | Runtime Stage |
| | | |
| - Full toolchain | ---> | - Minimal base |
| - Dev deps | | - Only artifacts |
| - Source code | | - No build tools |
| - Build output | | - No source code |
+-------------------+ +-------------------+
~800 MB ~80 MB
```
**Benefits**: Smaller images, faster deploys, reduced attack surface, no build tools in production.
## Base Image Selection
| Image | Size | Use Case | Security | Package Manager |
|-------|------|----------|----------|-----------------|
| **alpine** | ~5 MB | Small images, CLI tools | Good (small surface) | apk |
| **slim** (Debian) | ~80 MB | Most apps (Python, Node) | Good | apt |
| **distroless** | ~20 MB | Production, no shell needed | Excellent (no shell) | None |
| **scratch** | 0 MB | Static Go/Rust binaries | Excellent (nothing) | None |
| **full** (Debian) | ~300 MB | Build stages, debugging | Fair (large surface) | apt |
### Recommendations by Language
| Language | Build Stage | Runtime Stage |
|----------|-------------|---------------|
| **Python** | `python:3.12-slim` | `python:3.12-slim` or `distroless/python3` |
| **Node.js** | `node:22-slim` | `node:22-slim` or `distroless/nodejs22` |
| **Go** | `golang:1.23` | `scratch` or `distroless/static` |
| **Rust** | `rust:1.83` | `scratch` or `distroless/cc` |
| **Java** | `eclipse-temurin:21-jdk` | `eclipse-temurin:21-jre-alpine` |
## Instruction Best Practices
### RUN: Combine and Clean Up
```dockerfile
# BAD: Multiple layers, leftover cache
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
# GOOD: Single layer, cache cleaned
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
```
### COPY: Be Specific
```dockerfile
# BAD: Copies everything, including .git, node_modules, etc.
COPY . .
# GOOD: Copy only what's needed (use .dockerignore too)
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY src/ ./src/
COPY tsconfig.json ./
```
### .dockerignore Essentials
```
.git
node_modules
__pycache__
.env
*.log
dist
.venv
.pytest_cache
coverage
.DS_Store
```
### USER: Don't Run as Root
```dockerfile
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser -s /bin/false appuser
USER appuser
```
### HEALTHCHECK
```dockerfile
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
```
### ARG vs ENV
| Directive | Available at | Persists in image | Use for |
|-----------|-------------|-------------------|---------|
| `ARG` | Build time only | No | Build-time toggles, versions |
| `ENV` | Build + runtime | Yes | App configuration |
```dockerfile
ARG PYTHON_VERSION=3.12
FROM python:${PYTHON_VERSION}-slim
ENV APP_ENV=production
ENV PORT=8000
```
## Security Checklist
| Practice | Command/Example |
|----------|----------------|
| Pin base image digests | `FROM node:22-slim@sha256:abc123...` |
| Run as non-root | `USER appuser` |
| No secrets in layers | Use `--mount=type=secret` or build args |
| Scan for vulnerabilities | `docker scout cves`, `trivy image` |
| Read-only filesystem | `docker run --read-only` |
| Drop capabilities | `docker run --cap-drop ALL` |
| Use `.dockerignore` | Exclude `.env`, `.git`, credentials |
| Minimal base image | Use slim/distroless/scratch |
### Secrets at Build Time (BuildKit)
```dockerfile
# Mount a secret file without baking it into a layer
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
npm install
# Build command:
# docker build --secret id=npm_token,src=.npmrc .
```
## Image Size Reduction Checklist
1. Use multi-stage builds
2. Choose slim/alpine/distroless base
3. Combine RUN commands
4. Remove package manager caches (`rm -rf /var/lib/apt/lists/*`)
5. Use `.dockerignore`
6. Don't install dev dependencies in runtime stage
7. Remove unnecessary files after build
8. Use `--no-install-recommends` with apt
## Common Pitfalls
| Pitfall | Impact | Fix |
|---------|--------|-----|
| `COPY . .` before `npm install` | No dependency caching | Copy lockfile first, install, then copy source |
| Using `latest` tag | Non-reproducible builds | Pin specific version tags or digests |
| Secrets in `ENV` or `COPY` | Leaked in image layers | Use BuildKit secrets mount |
| Running as root | Security vulnerability | Add `USER` directive |
| No `.dockerignore` | Bloated context, slow builds | Add and maintain `.dockerignore` |
| Installing build tools in final stage | Bloated image | Use multi-stage; build in first stage |
| Not using `--frozen-lockfile` | Non-deterministic installs | Always use lockfile flags |
@@ -1,93 +0,0 @@
# =============================================================================
# Multi-Stage Node.js Dockerfile
# Usage:
# docker build -t myapp .
# docker run -p 3000:3000 myapp
# =============================================================================
# ---------------------------------------------------------------------------
# Stage 1: Install dependencies
# ---------------------------------------------------------------------------
FROM node:22-slim AS deps
# Enable corepack for pnpm support.
RUN corepack enable
WORKDIR /app
# Copy only package manifests first for dependency layer caching.
# Dependencies are only reinstalled when these files change.
COPY package.json pnpm-lock.yaml ./
# Install production and dev dependencies (dev deps needed for build step).
RUN pnpm install --frozen-lockfile
# ---------------------------------------------------------------------------
# Stage 2: Build the application
# ---------------------------------------------------------------------------
FROM node:22-slim AS builder
RUN corepack enable
WORKDIR /app
# Copy dependencies from the deps stage.
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/package.json /app/pnpm-lock.yaml ./
# Copy source code and config files needed for the build.
COPY tsconfig.json ./
COPY src/ ./src/
# COPY public/ ./public/ # Uncomment for Next.js or static assets
# Build the application.
RUN pnpm build
# Remove dev dependencies after build to reduce size.
RUN pnpm prune --prod
# ---------------------------------------------------------------------------
# Stage 3: Production runtime
# ---------------------------------------------------------------------------
FROM node:22-slim AS runtime
# Run as non-root for security.
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /bin/false appuser
WORKDIR /app
# Set production environment.
ENV NODE_ENV=production \
PORT=3000
# Copy only production artifacts from the builder stage.
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
# For Next.js standalone output, use instead:
# COPY --from=builder /app/.next/standalone ./
# COPY --from=builder /app/.next/static ./.next/static
# COPY --from=builder /app/public ./public
# Switch to non-root user.
USER appuser
# Expose the application port.
EXPOSE 3000
# Health check -- adjust the endpoint to match your app.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "fetch('http://localhost:3000/health').then(r => { if (!r.ok) process.exit(1) })" || exit 1
# Run the application.
CMD ["node", "dist/server.js"]
# For Next.js standalone:
# CMD ["node", "server.js"]
# For NestJS:
# CMD ["node", "dist/main.js"]
# For Express with ts-node (dev only, not recommended for production):
# CMD ["npx", "ts-node", "src/server.ts"]
@@ -1,78 +0,0 @@
# =============================================================================
# Multi-Stage Python Dockerfile
# Usage:
# docker build -t myapp .
# docker run -p 8000:8000 myapp
# =============================================================================
# ---------------------------------------------------------------------------
# Stage 1: Build dependencies
# ---------------------------------------------------------------------------
# Use slim for building - it has gcc and headers available via apt.
FROM python:3.12-slim AS builder
# Prevent Python from writing .pyc files and enable unbuffered output.
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Install build-time system dependencies (if any compiled packages need them).
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install Python dependencies into a virtual environment.
# Copying requirements first enables Docker layer caching --
# dependencies are only reinstalled when requirements.txt changes.
COPY requirements.txt .
RUN python -m venv /app/.venv \
&& /app/.venv/bin/pip install --no-cache-dir --upgrade pip \
&& /app/.venv/bin/pip install --no-cache-dir -r requirements.txt
# ---------------------------------------------------------------------------
# Stage 2: Runtime
# ---------------------------------------------------------------------------
FROM python:3.12-slim AS runtime
# Prevent .pyc files and enable unbuffered output for logging.
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/app/.venv/bin:$PATH"
# Install only runtime system dependencies (no build tools).
# Add packages here if your app needs them at runtime (e.g., libpq for psycopg).
# RUN apt-get update && apt-get install -y --no-install-recommends \
# libpq5 \
# && rm -rf /var/lib/apt/lists/*
# Create a non-root user for security.
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /bin/false appuser
WORKDIR /app
# Copy the virtual environment from the builder stage.
COPY --from=builder /app/.venv /app/.venv
# Copy application source code.
COPY src/ ./src/
# Switch to non-root user.
USER appuser
# Expose the application port.
EXPOSE 8000
# Health check -- adjust the endpoint to match your app.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
# Run the application.
# For FastAPI/Uvicorn:
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
# For Django/Gunicorn:
# CMD ["gunicorn", "src.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"]
# For a plain script:
# CMD ["python", "-m", "src.main"]
@@ -1,100 +0,0 @@
# =============================================================================
# Development Docker Compose
# Usage:
# docker compose -f docker-compose.dev.yaml up
# docker compose -f docker-compose.dev.yaml down -v # remove volumes too
# =============================================================================
services:
# ---------------------------------------------------------------------------
# Application
# ---------------------------------------------------------------------------
app:
build:
context: .
dockerfile: Dockerfile
target: builder # Use the build stage for dev (includes dev deps)
ports:
- "${APP_PORT:-3000}:3000"
environment:
NODE_ENV: development
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/app_dev
REDIS_URL: redis://redis:6379/0
volumes:
# Mount source code for hot-reload. Exclude node_modules.
- .:/app
- /app/node_modules
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
# Override CMD for development (hot-reload).
command: ["pnpm", "dev"]
# For Python:
# command: ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "3000", "--reload"]
# ---------------------------------------------------------------------------
# PostgreSQL
# ---------------------------------------------------------------------------
postgres:
image: postgres:17-alpine
ports:
- "${POSTGRES_PORT:-5432}:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_dev
volumes:
# Persist data across restarts.
- postgres_data:/var/lib/postgresql/data
# Run init scripts on first start.
# - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
# ---------------------------------------------------------------------------
# Redis
# ---------------------------------------------------------------------------
redis:
image: redis:7-alpine
ports:
- "${REDIS_PORT:-6379}:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
# Persist data to disk every 60 seconds if at least 1 key changed.
command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"]
# ---------------------------------------------------------------------------
# Optional: pgAdmin (database GUI)
# ---------------------------------------------------------------------------
# pgadmin:
# image: dpage/pgadmin4:latest
# ports:
# - "5050:80"
# environment:
# PGADMIN_DEFAULT_EMAIL: admin@local.dev
# PGADMIN_DEFAULT_PASSWORD: admin
# depends_on:
# - postgres
# ---------------------------------------------------------------------------
# Optional: Mailpit (email testing)
# ---------------------------------------------------------------------------
# mailpit:
# image: axllent/mailpit:latest
# ports:
# - "8025:8025" # Web UI
# - "1025:1025" # SMTP
volumes:
postgres_data:
redis_data:
@@ -1,250 +0,0 @@
# GitHub Actions Syntax Quick Reference
## Workflow File Structure
```yaml
name: CI # Workflow name (shown in GitHub UI)
on: # Triggers
push:
branches: [main]
pull_request:
branches: [main]
permissions: # Workflow-level permissions
contents: read
env: # Workflow-level environment variables
NODE_ENV: test
jobs:
build: # Job ID
runs-on: ubuntu-latest # Runner
steps:
- uses: actions/checkout@v4 # Action step
- run: echo "Hello" # Shell step
```
## Triggers (on:)
### Common Events
```yaml
on:
push:
branches: [main, "release/**"]
paths: ["src/**", "!src/**/*.test.ts"] # Path filtering
tags: ["v*"]
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
workflow_dispatch: # Manual trigger
inputs:
environment:
type: choice
options: [staging, production]
schedule:
- cron: "0 6 * * 1" # Every Monday at 6am UTC
release:
types: [published]
workflow_call: # Reusable workflow
inputs:
node-version: { type: string, default: "22" }
secrets:
NPM_TOKEN: { required: true }
```
## Jobs
```yaml
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- run: npm test
deploy:
needs: [lint, test] # Runs after lint AND test succeed
runs-on: ubuntu-latest
steps: [...]
```
### Matrix Strategy
```yaml
jobs:
test:
strategy:
fail-fast: false # Don't cancel other jobs on failure
matrix:
os: [ubuntu-latest, macos-latest]
node: [20, 22]
exclude:
- os: macos-latest
node: 20
include:
- os: ubuntu-latest
node: 22
coverage: true
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
```
## Steps
### Action Step
```yaml
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history (needed for some tools)
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"
```
### Shell Step
```yaml
- name: Run tests
run: npm test
working-directory: ./packages/api
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
shell: bash
continue-on-error: true # Don't fail the job
timeout-minutes: 10
```
### Multi-line Commands
```yaml
- run: |
echo "Line 1"
echo "Line 2"
npm run build
```
## Conditionals (if:)
```yaml
# Run only on main branch
- if: github.ref == 'refs/heads/main'
# Run only on pull requests
- if: github.event_name == 'pull_request'
# Run only when previous step failed
- if: failure()
# Always run (even if previous steps failed)
- if: always()
# Run only when a matrix variable is set
- if: matrix.coverage == true
# Run based on changed files (requires dorny/paths-filter or similar)
- if: steps.filter.outputs.backend == 'true'
# Run on specific actor
- if: github.actor != 'dependabot[bot]'
```
## Environment and Secrets
```yaml
jobs:
deploy:
environment:
name: production
url: https://example.com
env:
APP_VERSION: ${{ github.sha }}
steps:
- run: deploy.sh
env:
API_KEY: ${{ secrets.API_KEY }} # Repository secret
DEPLOY_TOKEN: ${{ vars.DEPLOY_TOKEN }} # Repository variable
```
## Caching
### Built-in Cache (setup actions)
```yaml
- uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm" # Automatic pnpm cache
```
### Manual Cache
```yaml
- uses: actions/cache@v4
with:
path: |
~/.cache/pip
.mypy_cache
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
```
## Artifacts
### Upload
```yaml
- uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 7
```
### Download (in another job)
```yaml
- uses: actions/download-artifact@v4
with:
name: coverage-report
path: ./coverage
```
## Services (Containers)
Define `services:` under a job with `image`, `env`, `ports`, and `options` (for health checks). Common: postgres, redis, mysql.
## Outputs (Passing Data Between Steps/Jobs)
```yaml
# Step output: echo "key=value" >> "$GITHUB_OUTPUT"
# Read in later step: ${{ steps.<step-id>.outputs.key }}
# Job output: declare under jobs.<job>.outputs, read via needs.<job>.outputs.key
```
## Permissions
Common values: `contents: read`, `pull-requests: write`, `issues: write`, `packages: write`, `id-token: write` (OIDC).
## Reusable Workflows
Caller uses `uses: ./.github/workflows/reusable.yaml` with `with:` and `secrets: inherit`. Callee triggers on `workflow_call:` with `inputs:` and `secrets:` definitions.
## Useful Expressions
| Expression | Result |
|-----------|--------|
| `${{ github.sha }}` | Full commit SHA |
| `${{ github.ref_name }}` | Branch or tag name |
| `${{ github.event.pull_request.number }}` | PR number |
| `${{ runner.os }}` | `Linux`, `macOS`, `Windows` |
| `${{ hashFiles('**/lockfile') }}` | SHA256 of files |
| `${{ contains(github.event.head_commit.message, '[skip ci]') }}` | Check commit message |
@@ -1,176 +0,0 @@
# =============================================================================
# Node.js CI Pipeline
# Runs: lint (eslint), type check (tsc), test (vitest), build
# =============================================================================
name: Node CI
on:
push:
branches: [main]
paths:
- "**.ts"
- "**.tsx"
- "**.js"
- "**.jsx"
- "package.json"
- "pnpm-lock.yaml"
- "tsconfig.json"
- ".github/workflows/ci-node.yaml"
pull_request:
branches: [main]
paths:
- "**.ts"
- "**.tsx"
- "**.js"
- "**.jsx"
- "package.json"
- "pnpm-lock.yaml"
- "tsconfig.json"
- ".github/workflows/ci-node.yaml"
permissions:
contents: read
env:
NODE_VERSION: "22"
jobs:
# ---------------------------------------------------------------------------
# Install dependencies (shared across jobs via cache)
# ---------------------------------------------------------------------------
install:
name: Install
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
# ---------------------------------------------------------------------------
# Lint with ESLint
# ---------------------------------------------------------------------------
lint:
name: Lint
needs: install
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm lint
# ---------------------------------------------------------------------------
# Type check with TypeScript compiler
# ---------------------------------------------------------------------------
type-check:
name: Type Check
needs: install
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm tsc --noEmit
# ---------------------------------------------------------------------------
# Test with Vitest
# ---------------------------------------------------------------------------
test:
name: Test
needs: install
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- name: Run tests with coverage
run: pnpm vitest run --coverage --reporter=junit --outputFile=junit.xml
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 7
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: junit.xml
retention-days: 7
# ---------------------------------------------------------------------------
# Build
# ---------------------------------------------------------------------------
build:
name: Build
needs: [lint, type-check, test]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 7
@@ -1,164 +0,0 @@
# =============================================================================
# Python CI Pipeline
# Runs: lint (ruff), type check (mypy), test (pytest), coverage upload
# =============================================================================
name: Python CI
on:
push:
branches: [main]
paths:
- "**.py"
- "requirements*.txt"
- "pyproject.toml"
- ".github/workflows/ci-python.yaml"
pull_request:
branches: [main]
paths:
- "**.py"
- "requirements*.txt"
- "pyproject.toml"
- ".github/workflows/ci-python.yaml"
permissions:
contents: read
env:
PYTHON_VERSION: "3.12"
jobs:
# ---------------------------------------------------------------------------
# Lint with Ruff
# ---------------------------------------------------------------------------
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install ruff
run: pip install ruff
- name: Ruff check (lint)
run: ruff check .
- name: Ruff format (formatting)
run: ruff format --check .
# ---------------------------------------------------------------------------
# Type check with mypy
# ---------------------------------------------------------------------------
type-check:
name: Type Check
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install mypy
- name: Run mypy
run: mypy src/ --ignore-missing-imports
# ---------------------------------------------------------------------------
# Test with pytest
# ---------------------------------------------------------------------------
test:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 15
# Uncomment to add a PostgreSQL service for integration tests.
# services:
# postgres:
# image: postgres:17-alpine
# env:
# POSTGRES_USER: test
# POSTGRES_PASSWORD: test
# POSTGRES_DB: test_db
# 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-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Run tests with coverage
run: |
pytest \
--cov=src \
--cov-report=xml:coverage.xml \
--cov-report=term-missing \
--junitxml=junit.xml \
-v
env:
PYTHONPATH: ${{ github.workspace }}
# DATABASE_URL: postgresql://test:test@localhost:5432/test_db
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage.xml
retention-days: 7
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: junit.xml
retention-days: 7
# Uncomment to upload coverage to Codecov.
# - name: Upload to Codecov
# if: github.event_name == 'push' && github.ref == 'refs/heads/main'
# uses: codecov/codecov-action@v4
# with:
# files: coverage.xml
# token: ${{ secrets.CODECOV_TOKEN }}
@@ -0,0 +1,545 @@
# DevOps — Cloudflare Workers Patterns
# Cloudflare Workers & Pages
## Overview
Edge-first deployment patterns for Cloudflare's platform. Covers Workers (compute), Pages (static + SSR), R2 (object storage), D1 (SQLite at edge), KV (key-value), Durable Objects (stateful), and Queues (async processing). Focused on the Python/TypeScript stack this kit targets.
## When to Use
- Deploying APIs or full-stack apps to Cloudflare's edge network
- Building serverless functions with Workers
- Deploying Next.js or static sites via Cloudflare Pages
- Using D1 (edge SQLite), R2 (S3-compatible storage), or KV (low-latency reads)
- Implementing real-time coordination with Durable Objects
- Background job processing with Cloudflare Queues
## When NOT to Use
- **Long-running compute** (> 30s CPU) — use traditional servers or containers
- **Heavy database workloads** — D1 is SQLite; use Postgres/Mongo for complex queries
- **GPU/ML inference** (unless using Workers AI) — use dedicated compute
- **Local-only development** — Workers run on V8 isolates, not Node.js
---
## Quick Reference
| I need... | Go to |
|-----------|-------|
| Worker project structure | § Project Structure below |
| Hono framework on Workers | § Hono Framework below |
| D1 database patterns | § D1 (Edge SQLite) below |
| R2 object storage | § R2 (Object Storage) below |
| KV key-value store | § KV below |
| Durable Objects | § Durable Objects below |
| Pages deployment (Next.js) | § Cloudflare Pages below |
| CI/CD with GitHub Actions | § CI/CD below |
| Wrangler config reference | See `wrangler-patterns.md` in this skill's directory |
---
## Project Structure
```
my-worker/
├── wrangler.toml # Wrangler config (bindings, routes, env)
├── src/
│ ├── index.ts # Entry point (fetch handler)
│ ├── routes/ # Route handlers
│ ├── middleware/ # Auth, CORS, logging
│ ├── services/ # Business logic
│ └── types.ts # Env bindings type
├── migrations/ # D1 migrations
├── test/ # Vitest tests
└── package.json
```
### Entry point
```typescript
// src/index.ts
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/health') {
return Response.json({ status: 'ok' });
}
// Route to handlers...
return new Response('Not found', { status: 404 });
},
} satisfies ExportedHandler<Env>;
```
### Type-safe bindings
```typescript
// src/types.ts
export interface Env {
DB: D1Database;
BUCKET: R2Bucket;
CACHE: KVNamespace;
API_KEY: string;
ENVIRONMENT: 'development' | 'staging' | 'production';
}
```
---
## Hono Framework (Recommended)
Hono is the de facto framework for Workers — ultralight (~14KB), type-safe, and built for edge runtimes.
```typescript
// src/index.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { HTTPException } from 'hono/http-exception';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
type Bindings = {
DB: D1Database;
BUCKET: R2Bucket;
API_KEY: string;
};
const app = new Hono<{ Bindings: Bindings }>();
app.use('*', logger());
app.use('*', cors({ origin: ['https://app.example.com'], credentials: true }));
// Health check
app.get('/health', (c) => c.json({ status: 'ok' }));
// Validated endpoint
const createUserSchema = z.object({
email: z.string().email().max(254),
name: z.string().min(1).max(100),
});
app.post('/v1/users', zValidator('json', createUserSchema), async (c) => {
const { email, name } = c.req.valid('json');
const result = await c.env.DB
.prepare('INSERT INTO users (id, email, name) VALUES (?, ?, ?) RETURNING *')
.bind(crypto.randomUUID(), email, name)
.first();
return c.json(result, 201);
});
// Error handling — RFC 9457 Problem Details
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({
type: `https://api.example.com/problems/${err.status}`,
title: err.message,
status: err.status,
}, err.status);
}
console.error(err);
return c.json({
type: 'https://api.example.com/problems/internal-error',
title: 'Internal server error',
status: 500,
}, 500);
});
export default app;
```
---
## D1 (Edge SQLite)
Cloudflare's serverless SQL database. SQLite at the edge with automatic replication.
### Migrations
```bash
# Create migration
npx wrangler d1 migrations create my-db create-users
# Apply locally
npx wrangler d1 migrations apply my-db --local
# Apply to production
npx wrangler d1 migrations apply my-db --remote
```
```sql
-- migrations/0001_create-users.sql
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
role TEXT DEFAULT 'member' CHECK(role IN ('admin', 'member', 'viewer')),
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX idx_users_email ON users(email);
```
### Querying with prepared statements
```typescript
// Always use prepared statements — never concatenate SQL
async function getUser(db: D1Database, id: string) {
return db.prepare('SELECT * FROM users WHERE id = ?').bind(id).first();
}
async function listUsers(db: D1Database, cursor?: string, limit = 20) {
const stmt = cursor
? db.prepare('SELECT * FROM users WHERE id > ? ORDER BY id LIMIT ?').bind(cursor, limit)
: db.prepare('SELECT * FROM users ORDER BY id LIMIT ?').bind(limit);
return stmt.all();
}
// Batch multiple statements in a transaction
async function transferCredits(db: D1Database, from: string, to: string, amount: number) {
const results = await db.batch([
db.prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?').bind(amount, from),
db.prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?').bind(amount, to),
]);
return results;
}
```
### D1 limitations to know
- **No JOINs across databases** — one D1 database per binding
- **5MB max row size**, 10GB max database
- **Read replicas are automatic** but writes go to a single leader
- **No stored procedures / triggers** — SQLite subset
- **Prepared statements are mandatory** — `db.exec()` with raw SQL is for migrations only
---
## R2 (Object Storage)
S3-compatible object storage without egress fees.
```typescript
// Upload
app.put('/v1/files/:key', async (c) => {
const key = c.req.param('key');
const body = await c.req.arrayBuffer();
const contentType = c.req.header('Content-Type') ?? 'application/octet-stream';
await c.env.BUCKET.put(key, body, {
httpMetadata: { contentType },
customMetadata: { uploadedBy: c.get('userId') },
});
return c.json({ key, size: body.byteLength }, 201);
});
// Download
app.get('/v1/files/:key', async (c) => {
const obj = await c.env.BUCKET.get(c.req.param('key'));
if (!obj) return c.json({ error: 'Not found' }, 404);
return new Response(obj.body, {
headers: {
'Content-Type': obj.httpMetadata?.contentType ?? 'application/octet-stream',
'ETag': obj.etag,
},
});
});
// List with prefix
app.get('/v1/files', async (c) => {
const prefix = c.req.query('prefix') ?? '';
const listed = await c.env.BUCKET.list({ prefix, limit: 100 });
return c.json({ objects: listed.objects.map((o) => ({ key: o.key, size: o.size })) });
});
```
### Presigned URLs for direct upload
```typescript
// Generate a presigned URL so clients upload directly to R2
app.post('/v1/upload-url', async (c) => {
const key = `uploads/${crypto.randomUUID()}`;
// Use the S3-compatible API for presigned URLs
// Requires R2 API token with write access
return c.json({ key, uploadUrl: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com/${BUCKET_NAME}/${key}` });
});
```
---
## KV (Key-Value Store)
Global low-latency reads (~10ms worldwide), eventually consistent writes.
```typescript
// Set with TTL
await c.env.CACHE.put('session:abc123', JSON.stringify(sessionData), {
expirationTtl: 3600, // 1 hour
});
// Get with type safety
const raw = await c.env.CACHE.get('session:abc123');
const session = raw ? JSON.parse(raw) as SessionData : null;
// List keys by prefix
const keys = await c.env.CACHE.list({ prefix: 'session:' });
// Delete
await c.env.CACHE.delete('session:abc123');
```
**Use KV for:** session tokens, feature flags, cached API responses, configuration. **Not for:** frequently updated counters, multi-key transactions (use Durable Objects).
---
## Durable Objects
Stateful, single-instance coordination. Each Durable Object has a unique ID and runs in exactly one location.
```typescript
// src/counter.ts
export class Counter implements DurableObject {
private count = 0;
constructor(private state: DurableObjectState, private env: Env) {}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/increment') {
this.count++;
await this.state.storage.put('count', this.count);
return Response.json({ count: this.count });
}
this.count = (await this.state.storage.get<number>('count')) ?? 0;
return Response.json({ count: this.count });
}
}
// In the Worker, route to the Durable Object:
app.post('/v1/counters/:name/increment', async (c) => {
const id = c.env.COUNTER.idFromName(c.req.param('name'));
const stub = c.env.COUNTER.get(id);
const res = await stub.fetch(new Request('https://dummy/increment'));
return c.json(await res.json());
});
```
**Use Durable Objects for:** rate limiting, WebSocket rooms, collaborative editing, distributed locks, shopping carts. **Not for:** read-heavy caching (use KV).
---
## Cloudflare Pages
### Next.js on Pages
```bash
# Deploy Next.js to Cloudflare Pages
npx wrangler pages deploy .next --project-name=my-app
```
Use `@cloudflare/next-on-pages` for full App Router + Server Components support:
```bash
pnpm add @cloudflare/next-on-pages
```
```typescript
// next.config.ts
import { setupDevPlatform } from '@cloudflare/next-on-pages/next-dev';
if (process.env.NODE_ENV === 'development') {
await setupDevPlatform();
}
const nextConfig = { /* ... */ };
export default nextConfig;
```
### Static site on Pages
```bash
# Build and deploy
pnpm build
npx wrangler pages deploy dist/ --project-name=my-site
```
Pages auto-deploys from GitHub: connect your repo in the Cloudflare dashboard, set the build command and output directory. Preview deploys on every PR.
---
## Wrangler Config
```toml
# wrangler.toml
name = "my-api"
main = "src/index.ts"
compatibility_date = "2026-01-01"
compatibility_flags = ["nodejs_compat"]
[vars]
ENVIRONMENT = "production"
# D1 database
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# R2 bucket
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-bucket"
# KV namespace
[[kv_namespaces]]
binding = "CACHE"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Durable Object
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
[[migrations]]
tag = "v1"
new_classes = ["Counter"]
# Environment overrides
[env.staging]
vars = { ENVIRONMENT = "staging" }
[env.staging.d1_databases]
binding = "DB"
database_name = "my-db-staging"
database_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
```
**`compatibility_date`** pins your Worker to a specific runtime version. Always set it to a recent date and update periodically. **`nodejs_compat`** enables Node.js built-in APIs (Buffer, crypto, streams) — required for most npm packages.
---
## CI/CD
### GitHub Actions deploy
```yaml
# .github/workflows/deploy.yml
name: Deploy Worker
on:
push:
branches: [main]
pull_request:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: pnpm install
- name: Run tests
run: pnpm test
- name: Apply D1 migrations (production)
if: github.ref == 'refs/heads/main'
run: npx wrangler d1 migrations apply my-db --remote
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
- name: Deploy to staging (PR)
if: github.event_name == 'pull_request'
run: npx wrangler deploy --env staging
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
- name: Deploy to production
if: github.ref == 'refs/heads/main'
run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
```
### Local development
```bash
# Start local dev server with all bindings (D1, R2, KV, DO)
npx wrangler dev
# With local D1 persistence
npx wrangler dev --persist-to .wrangler/state
```
`wrangler dev` uses Miniflare under the hood — a local simulator for all Cloudflare primitives. Test against real bindings locally before deploying.
---
## Testing
Use **Vitest + Miniflare** (via `@cloudflare/vitest-pool-workers`):
```typescript
// vitest.config.ts
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
export default defineWorkersConfig({
test: {
poolOptions: {
workers: {
wrangler: { configPath: './wrangler.toml' },
},
},
},
});
```
```typescript
// test/index.spec.ts
import { env, createExecutionContext, waitOnExecutionContext } from 'cloudflare:test';
import { describe, it, expect } from 'vitest';
import worker from '../src/index';
describe('Worker', () => {
it('returns health check', async () => {
const request = new Request('http://localhost/health');
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(200);
const body = await response.json();
expect(body).toEqual({ status: 'ok' });
});
});
```
---
## Common Pitfalls
1. **Using Node.js APIs without `nodejs_compat`.** Workers run on V8, not Node.js. Without the flag, `Buffer`, `crypto`, `process` are undefined.
2. **Blocking the event loop.** Workers have strict CPU time limits (10ms free, 30s paid). Heavy computation blocks all concurrent requests. Use `ctx.waitUntil()` for background work.
3. **Ignoring D1's eventually consistent reads.** Writes go to the leader; reads from replicas may lag by seconds. Design for eventual consistency.
4. **Using KV for frequently updated data.** KV is eventually consistent with ~60s propagation. Use Durable Objects for strong consistency.
5. **Not setting `compatibility_date`.** Without it, you get the oldest runtime behavior. Always pin to a recent date.
6. **Forgetting `ctx.waitUntil()`.** Background work (logging, analytics) must be wrapped in `waitUntil()` or it gets killed when the response is sent.
7. **Large Worker bundles.** Workers have a 10MB compressed limit (free: 1MB). Tree-shake aggressively; avoid heavy npm packages.
8. **Not testing locally with Miniflare.** `wrangler dev` simulates all bindings locally. Deploying untested changes to edge = debugging in production.
---
## Related Skills
- `openapi` — API design (Workers APIs benefit from the same conventions)
- `docker` — alternative deployment model (containers vs edge)
- `github-actions` — CI/CD pipeline for deploying Workers
- `typescript` — TypeScript patterns (Workers are TypeScript-first)
- `vitest` — testing Workers with Miniflare pool
@@ -1,8 +1,5 @@
---
name: docker
description: >
Use this skill whenever containerizing applications, writing Dockerfiles, configuring Docker Compose, or optimizing container images. Trigger on keywords like Docker, Dockerfile, container, docker-compose, multi-stage build, image, or container registry. Also applies when setting up local development environments with containers, debugging container networking, or preparing applications for container-based deployment in CI/CD pipelines.
---
# DevOps — Docker Patterns
# Docker
@@ -654,6 +651,6 @@ services:
## Related Skills
- `devops/github-actions` - CI/CD workflows for building and deploying Docker containers
- `security/owasp` - Security best practices for container hardening and vulnerability scanning
- `patterns/logging` — Container logging and log aggregation
- `github-actions` - CI/CD workflows for building and deploying Docker containers
- `owasp` - Security best practices for container hardening and vulnerability scanning
- `logging` — Container logging and log aggregation
@@ -1,8 +1,5 @@
---
name: github-actions
description: >
Use this skill whenever setting up or modifying GitHub Actions CI/CD workflows, automating tests, builds, or deployments on GitHub. Trigger on keywords like GitHub Actions, workflow YAML, CI/CD pipeline, actions/checkout, matrix builds, workflow_dispatch, or .github/workflows. Also applies when configuring caching in workflows, managing GitHub secrets, or troubleshooting failed workflow runs.
---
# DevOps — GitHub Actions Patterns
# GitHub Actions
@@ -799,6 +796,6 @@ jobs:
## Related Skills
- `devops/docker` - Container patterns for building and deploying Dockerized applications in workflows
- `testing/pytest` - Python test configuration for CI pipeline integration
- `testing/vitest` - TypeScript/JavaScript test configuration for CI pipeline integration
- `docker` - Container patterns for building and deploying Dockerized applications in workflows
- `pytest` - Python test configuration for CI pipeline integration
- `vitest` - TypeScript/JavaScript test configuration for CI pipeline integration
@@ -1,7 +1,7 @@
---
name: dispatching-parallel-agents
description: >
Trigger this skill when facing 3 or more independent failures across different domains, when multiple subsystems are broken with no shared state, or when test failures span unrelated modules. Use whenever you see independent bugs in auth, cart, user, or other separate domains that can be fixed concurrently. Activate aggressively for any scenario where parallel work would reduce total resolution time without creating merge conflicts.
Use when facing 3 or more independent failures across different domains, when multiple subsystems are broken with no shared state, or when test failures span unrelated modules. Also activate whenever you see independent bugs in auth, cart, user, or other separate domains that can be fixed concurrently. Activate aggressively for any scenario where parallel work would reduce total resolution time without creating merge conflicts.
---
# Dispatching Parallel Agents
@@ -229,6 +229,69 @@ git merge agent-3-user-fixes
---
## Example: Full-Stack Feature Dispatch
A real-world example dispatching 3 agents for a new "orders" feature:
### Independence check
| | Agent 1 (Backend) | Agent 2 (Frontend) | Agent 3 (Database) |
|---|---|---|---|
| **Files** | `src/api/orders.py`, `tests/test_orders.py` | `src/components/order-form.tsx`, `*.test.tsx` | `migrations/003_orders.sql`, `tests/test_migration.py` |
| **Test suite** | `pytest tests/test_orders.py` | `npm test -- order-form` | `pytest tests/test_migration.py` |
| **Shared state?** | No | No | No |
All three touch different files and different test suites — safe to parallelize.
### Agent 1 — Backend (FastAPI)
```markdown
## Task: Implement POST /api/orders with validation
**Context**: FastAPI + SQLAlchemy async + Pydantic v2
**Files**: src/api/orders.py, src/schemas/order.py, tests/test_orders.py
**Constraints**: Depends(get_db), return 201, RFC 9457 errors
**Verify**: pytest tests/test_orders.py -v
```
### Agent 2 — Frontend (React/Next.js)
```markdown
## Task: Build OrderForm component with validation
**Context**: Next.js App Router + react-hook-form + Zod + shadcn/ui
**Files**: src/components/order-form.tsx, src/components/order-form.test.tsx
**Constraints**: 'use client', Zod schema, accessible form fields
**Verify**: npx vitest run src/components/order-form.test.tsx
```
### Agent 3 — Database (PostgreSQL)
```markdown
## Task: Create orders table migration
**Context**: Alembic migrations, PostgreSQL
**Files**: migrations/003_create_orders.sql, tests/test_orders_migration.py
**Constraints**: Include indexes on user_id and created_at, add foreign key to users
**Verify**: pytest tests/test_orders_migration.py -v
```
### Integration after all 3 complete
```bash
# 1. Run each agent's test suite to confirm
pytest tests/test_orders.py tests/test_orders_migration.py -v
npx vitest run src/components/order-form.test.tsx
# 2. Run full test suite for regressions
pytest -v && npm test
# 3. Verify no file conflicts
git diff --name-only # should show no overlapping files between agents
```
---
## Conflict Resolution
If conflicts detected:
@@ -262,5 +325,5 @@ After parallel completion:
## Related Skills
- `methodology/executing-plans` - Use executing-plans when tasks are sequential; use dispatching-parallel-agents when tasks are independent and can run concurrently
- `methodology/writing-plans` - Write a plan first to identify which tasks are independent before dispatching parallel agents
- `executing-plans` - Use executing-plans when tasks are sequential; use dispatching-parallel-agents when tasks are independent and can run concurrently
- `writing-plans` - Write a plan first to identify which tasks are independent before dispatching parallel agents
+90
View File
@@ -0,0 +1,90 @@
---
name: error-handling
description: >
Use when writing try/catch blocks, creating custom error classes, implementing retry logic, designing error boundaries in React, building API error responses, or handling failures gracefully. Also activate for any code dealing with exceptions, error propagation, graceful degradation, or fault tolerance.
---
# Error Handling Patterns
## When to Use
- Building API endpoints that must return consistent error responses
- Creating custom exception hierarchies for a domain model
- Implementing retry logic for unreliable network calls or external services
- Designing React error boundaries for component-level fault isolation
- Wrapping third-party libraries that throw unpredictable errors
- Converting between error representations at architectural boundaries (e.g., domain errors to HTTP errors)
- Adopting the Result pattern to avoid exceptions for expected failure paths
## When NOT to Use
- Simple one-off scripts or throwaway prototypes where unhandled crashes are acceptable
- Configuration files, static data, or declarative markup with no runtime logic
- Pure data transformation functions where invalid input should be prevented by types, not caught at runtime
---
## Quick Reference
| Pattern | Description |
|---------|-------------|
| Custom Error Classes | Domain-specific error hierarchy with error codes, messages, and detail metadata |
| Error Boundaries (React) | Component-level fault isolation using `react-error-boundary` or class-based boundaries |
| Retry with Backoff | Exponential backoff + jitter decorator/wrapper for transient failures |
| Circuit Breaker | Short-circuit calls to unhealthy dependencies, fall back to degraded state |
| Feature-Flag Degradation | Graceful UI/service degradation controlled by feature flags |
| API Error Responses | Consistent RFC 7807 Problem Details payloads with global exception handlers |
| Error Logging | Structured context (request ID, error code, stack trace) for observability |
| Result Pattern | Discriminated union / Result type for expected failure paths without exceptions |
## Language References
See `references/python-patterns.md` for Python examples.
See `references/typescript-patterns.md` for TypeScript/React examples.
---
## Best Practices
1. **Catch specific exceptions, not bare `except` or `catch`.** A catch-all hides bugs. Catch only the errors you know how to handle and let everything else propagate.
2. **Translate errors at architectural boundaries.** A database `IntegrityError` should become a domain `DuplicateEntryError` at the repository layer, then an HTTP 409 at the API layer. Each layer speaks its own error language.
3. **Preserve the original cause.** Always chain the original exception (`raise X from original` in Python, `{ cause }` in TypeScript) so the root cause is visible in logs and debuggers.
4. **Fail fast, recover high.** Detect errors as early as possible (validate inputs at the boundary) but handle them at the highest level that has enough context to decide what to do (e.g., return an HTTP response, show a fallback UI).
5. **Never swallow errors silently.** An empty `except: pass` or `catch {}` is almost always a bug. At minimum, log the error. If you intentionally ignore it, leave a comment explaining why.
6. **Use the Result pattern for expected failures.** When a function can legitimately fail (parsing, validation, lookups), return a Result instead of throwing. Reserve exceptions for truly unexpected situations.
7. **Make errors actionable.** Every error message should help the reader fix the problem. Include what happened, what was expected, and what the caller can do about it. `"User not found"` is worse than `"User with id '123' not found. Verify the id and check that the user has not been deleted."`.
8. **Test the error paths.** Write explicit tests for every error branch. Verify the error type, message, and status code. Error paths that are never tested are error paths that will break in production.
---
## Common Pitfalls
1. **Catching too broadly.** Using `except Exception` or `catch (e: any)` silences programming errors like `TypeError` or `ReferenceError` that should crash loudly during development.
2. **Logging and re-throwing without deduplication.** If every layer logs the same error, you get five log entries for one failure. Log at the outermost handler and let inner layers propagate.
3. **Returning error data in the wrong shape.** Mixing `{ error: "..." }`, `{ message: "..." }`, and `{ errors: [...] }` across endpoints forces every client to handle multiple formats. Pick one shape and enforce it globally.
4. **Leaking internal details to clients.** Stack traces, database table names, and file paths in API responses are a security risk. Sanitize errors before they leave the server.
5. **Retrying non-idempotent operations.** Retrying a `POST /orders` that partially succeeded can create duplicate orders. Only retry operations that are safe to repeat, or use idempotency keys.
6. **Ignoring async error boundaries.** In React, error boundaries do not catch errors inside event handlers or async callbacks. Use try/catch inside `onClick`, `useEffect` cleanup, and promise chains separately.
---
## Related Skills
- `logging` - Structured logging setup and conventions
- `api-client` - HTTP client wrappers with built-in error handling
- `owasp` - Preventing information leakage through error messages
- `python` - Python exception syntax and idioms
- `typescript` - TypeScript error types and narrowing
@@ -0,0 +1,388 @@
# Error Handling — Python Patterns
## 1. Custom Error Classes
Define a hierarchy of domain-specific errors so callers can catch at the right granularity.
```python
from enum import Enum
class ErrorCode(str, Enum):
"""Central registry of machine-readable error codes."""
NOT_FOUND = "NOT_FOUND"
VALIDATION_FAILED = "VALIDATION_FAILED"
DUPLICATE_ENTRY = "DUPLICATE_ENTRY"
UNAUTHORIZED = "UNAUTHORIZED"
RATE_LIMITED = "RATE_LIMITED"
EXTERNAL_SERVICE = "EXTERNAL_SERVICE"
INTERNAL = "INTERNAL"
class AppError(Exception):
"""Base error for the entire application.
All domain errors inherit from this so a single except clause
can catch everything the application intentionally raises.
"""
def __init__(
self,
message: str,
code: ErrorCode = ErrorCode.INTERNAL,
*,
details: dict | None = None,
cause: Exception | None = None,
) -> None:
super().__init__(message)
self.code = code
self.details = details or {}
if cause:
self.__cause__ = cause
def to_dict(self) -> dict:
return {
"error": self.code.value,
"message": str(self),
"details": self.details,
}
class NotFoundError(AppError):
def __init__(self, resource: str, identifier: str) -> None:
super().__init__(
f"{resource} with id '{identifier}' not found",
code=ErrorCode.NOT_FOUND,
details={"resource": resource, "id": identifier},
)
class ValidationError(AppError):
def __init__(self, field: str, reason: str) -> None:
super().__init__(
f"Validation failed for '{field}': {reason}",
code=ErrorCode.VALIDATION_FAILED,
details={"field": field, "reason": reason},
)
class ExternalServiceError(AppError):
def __init__(self, service: str, cause: Exception) -> None:
super().__init__(
f"External service '{service}' failed: {cause}",
code=ErrorCode.EXTERNAL_SERVICE,
cause=cause,
details={"service": service},
)
```
## 2. Retry Pattern
Retry transient failures with exponential backoff and jitter to avoid thundering herd.
```python
import asyncio
import random
import logging
from functools import wraps
from typing import TypeVar, Callable, Awaitable
logger = logging.getLogger(__name__)
T = TypeVar("T")
def retry(
max_attempts: int = 3,
base_delay: float = 1.0,
max_delay: float = 30.0,
retryable: tuple[type[Exception], ...] = (Exception,),
) -> Callable:
"""Retry decorator with exponential backoff and full jitter.
Args:
max_attempts: Total number of attempts including the first call.
base_delay: Initial delay in seconds before the first retry.
max_delay: Upper bound on the computed delay.
retryable: Exception types eligible for retry.
"""
def decorator(fn: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
@wraps(fn)
async def wrapper(*args, **kwargs) -> T:
last_exc: Exception | None = None
for attempt in range(1, max_attempts + 1):
try:
return await fn(*args, **kwargs)
except retryable as exc:
last_exc = exc
if attempt == max_attempts:
break
delay = min(base_delay * (2 ** (attempt - 1)), max_delay)
jitter = random.uniform(0, delay)
logger.warning(
"Attempt %d/%d failed (%s), retrying in %.2fs",
attempt,
max_attempts,
exc,
jitter,
)
await asyncio.sleep(jitter)
raise last_exc # type: ignore[misc]
return wrapper
return decorator
# Usage
@retry(max_attempts=3, base_delay=0.5, retryable=(ConnectionError, TimeoutError))
async def fetch_remote_config(url: str) -> dict:
async with httpx.AsyncClient(timeout=5) as client:
resp = await client.get(url)
resp.raise_for_status()
return resp.json()
```
## 3. Graceful Degradation — Circuit Breaker
When a dependency fails, fall back to a degraded but functional state instead of crashing.
```python
import time
from enum import Enum
class CircuitState(Enum):
CLOSED = "closed" # normal operation
OPEN = "open" # failing, reject immediately
HALF_OPEN = "half_open" # testing recovery
class CircuitBreaker:
"""Prevents cascading failures by short-circuiting calls to an unhealthy dependency."""
def __init__(
self,
failure_threshold: int = 5,
recovery_timeout: float = 30.0,
) -> None:
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.state = CircuitState.CLOSED
self.failure_count = 0
self.last_failure_time = 0.0
def _trip(self) -> None:
self.state = CircuitState.OPEN
self.last_failure_time = time.monotonic()
def _reset(self) -> None:
self.state = CircuitState.CLOSED
self.failure_count = 0
async def call(self, fn, *args, fallback=None, **kwargs):
if self.state == CircuitState.OPEN:
if time.monotonic() - self.last_failure_time > self.recovery_timeout:
self.state = CircuitState.HALF_OPEN
else:
if fallback is not None:
return fallback() if callable(fallback) else fallback
raise ExternalServiceError("circuit-breaker", RuntimeError("Circuit open"))
try:
result = await fn(*args, **kwargs)
if self.state == CircuitState.HALF_OPEN:
self._reset()
return result
except Exception as exc:
self.failure_count += 1
if self.failure_count >= self.failure_threshold:
self._trip()
raise
# Usage
recommendations_circuit = CircuitBreaker(failure_threshold=3, recovery_timeout=60)
async def get_recommendations(user_id: str) -> list[dict]:
return await recommendations_circuit.call(
recommendation_service.fetch,
user_id,
fallback=lambda: [], # empty list when service is down
)
```
## 4. API Error Responses (FastAPI)
Return consistent, machine-readable error payloads following RFC 7807 Problem Details.
```python
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from starlette.status import (
HTTP_400_BAD_REQUEST,
HTTP_404_NOT_FOUND,
HTTP_429_TOO_MANY_REQUESTS,
HTTP_500_INTERNAL_SERVER_ERROR,
)
app = FastAPI()
# Map domain error codes to HTTP status codes
STATUS_MAP: dict[ErrorCode, int] = {
ErrorCode.NOT_FOUND: HTTP_404_NOT_FOUND,
ErrorCode.VALIDATION_FAILED: HTTP_400_BAD_REQUEST,
ErrorCode.DUPLICATE_ENTRY: 409,
ErrorCode.UNAUTHORIZED: 401,
ErrorCode.RATE_LIMITED: HTTP_429_TOO_MANY_REQUESTS,
ErrorCode.EXTERNAL_SERVICE: 502,
ErrorCode.INTERNAL: HTTP_500_INTERNAL_SERVER_ERROR,
}
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError) -> JSONResponse:
status = STATUS_MAP.get(exc.code, 500)
return JSONResponse(
status_code=status,
content={
"type": f"https://docs.example.com/errors/{exc.code.value.lower()}",
"title": exc.code.value.replace("_", " ").title(),
"status": status,
"detail": str(exc),
**exc.details,
},
)
@app.exception_handler(Exception)
async def unhandled_error_handler(request: Request, exc: Exception) -> JSONResponse:
# Never leak internal details to the client
logger.exception("Unhandled exception on %s %s", request.method, request.url.path)
return JSONResponse(
status_code=500,
content={
"type": "https://docs.example.com/errors/internal",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred.",
},
)
```
## 5. Error Logging Integration
Attach structured context to errors so they are searchable and actionable in observability tools.
```python
import logging
import traceback
from contextvars import ContextVar
request_id_var: ContextVar[str] = ContextVar("request_id", default="unknown")
class StructuredErrorLogger:
"""Wraps the standard logger to attach error context automatically."""
def __init__(self, name: str) -> None:
self.logger = logging.getLogger(name)
def error(
self,
msg: str,
*,
exc: Exception | None = None,
extra: dict | None = None,
) -> None:
context = {
"request_id": request_id_var.get(),
**(extra or {}),
}
if exc is not None:
context["error_type"] = type(exc).__name__
context["error_message"] = str(exc)
context["stacktrace"] = traceback.format_exception(exc)
if isinstance(exc, AppError):
context["error_code"] = exc.code.value
context["error_details"] = exc.details
self.logger.error(msg, extra={"structured": context}, exc_info=exc)
# Usage in a FastAPI middleware
from starlette.middleware.base import BaseHTTPMiddleware
import uuid
log = StructuredErrorLogger(__name__)
class ErrorLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
rid = request.headers.get("x-request-id", str(uuid.uuid4()))
request_id_var.set(rid)
try:
response = await call_next(request)
return response
except Exception as exc:
log.error(
"Request failed",
exc=exc,
extra={"method": request.method, "path": request.url.path},
)
raise
```
## 6. Result Pattern
Use a Result type for operations where failure is an expected outcome. Avoids exception overhead and makes the failure path explicit in the type signature.
```python
# pip install result
from result import Ok, Err, Result
def parse_age(value: str) -> Result[int, str]:
"""Parse a string to a valid age. Returns Err for invalid input."""
try:
age = int(value)
except ValueError:
return Err(f"'{value}' is not a number")
if age < 0 or age > 150:
return Err(f"Age {age} is out of valid range (0-150)")
return Ok(age)
def validate_registration(data: dict) -> Result[dict, list[str]]:
"""Validate all fields, collecting every error instead of failing on the first."""
errors: list[str] = []
match parse_age(data.get("age", "")):
case Ok(age):
data["age"] = age
case Err(msg):
errors.append(msg)
name = data.get("name", "").strip()
if not name:
errors.append("Name is required")
if len(name) > 100:
errors.append("Name must be 100 characters or fewer")
if errors:
return Err(errors)
return Ok(data)
# Caller handles both paths explicitly
match validate_registration(form_data):
case Ok(valid):
user = create_user(valid)
case Err(errs):
return {"errors": errs}, 400
```
@@ -0,0 +1,451 @@
# Error Handling — TypeScript Patterns
## 1. Custom Error Classes
Define a hierarchy of domain-specific errors so callers can catch at the right granularity.
```typescript
// error-codes.ts
export const ErrorCode = {
NOT_FOUND: "NOT_FOUND",
VALIDATION_FAILED: "VALIDATION_FAILED",
DUPLICATE_ENTRY: "DUPLICATE_ENTRY",
UNAUTHORIZED: "UNAUTHORIZED",
RATE_LIMITED: "RATE_LIMITED",
EXTERNAL_SERVICE: "EXTERNAL_SERVICE",
INTERNAL: "INTERNAL",
} as const;
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
// app-error.ts
export class AppError extends Error {
public readonly code: ErrorCode;
public readonly details: Record<string, unknown>;
constructor(
message: string,
code: ErrorCode = ErrorCode.INTERNAL,
details: Record<string, unknown> = {},
options?: ErrorOptions
) {
super(message, options);
this.name = "AppError";
this.code = code;
this.details = details;
}
toJSON() {
return {
error: this.code,
message: this.message,
details: this.details,
};
}
}
export class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} with id '${id}' not found`, ErrorCode.NOT_FOUND, {
resource,
id,
});
this.name = "NotFoundError";
}
}
export class ValidationError extends AppError {
constructor(field: string, reason: string) {
super(
`Validation failed for '${field}': ${reason}`,
ErrorCode.VALIDATION_FAILED,
{ field, reason }
);
this.name = "ValidationError";
}
}
export class ExternalServiceError extends AppError {
constructor(service: string, cause: Error) {
super(
`External service '${service}' failed: ${cause.message}`,
ErrorCode.EXTERNAL_SERVICE,
{ service },
{ cause }
);
this.name = "ExternalServiceError";
}
}
```
## 2. Error Boundaries (React)
Isolate component failures so a single broken widget does not take down the whole page.
**Using react-error-boundary (recommended)**
```typescript
import {
ErrorBoundary,
type FallbackProps,
} from "react-error-boundary";
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert" className="rounded border border-red-300 bg-red-50 p-4">
<h2 className="font-semibold text-red-800">Something went wrong</h2>
<pre className="mt-2 text-sm text-red-700">{error.message}</pre>
<button
onClick={resetErrorBoundary}
className="mt-3 rounded bg-red-600 px-3 py-1 text-white"
>
Try again
</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => {
// Send to error tracking service
reportError({ error, componentStack: info.componentStack });
}}
onReset={() => {
// Clear any stale state before retry
queryClient.clear();
}}
>
<Dashboard />
</ErrorBoundary>
);
}
```
**Granular boundaries per feature**
```typescript
function DashboardPage() {
return (
<div className="grid grid-cols-3 gap-4">
{/* Each widget fails independently */}
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
<RevenueChart />
</ErrorBoundary>
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
<UserActivityFeed />
</ErrorBoundary>
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
<SystemHealthPanel />
</ErrorBoundary>
</div>
);
}
function WidgetErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div className="flex flex-col items-center justify-center rounded border border-dashed border-gray-300 p-6 text-gray-500">
<p>This widget failed to load.</p>
<button onClick={resetErrorBoundary} className="mt-2 underline">
Retry
</button>
</div>
);
}
```
**Class-based error boundary (when you need full control)**
```typescript
import { Component, type ErrorInfo, type ReactNode } from "react";
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ManualErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error("ErrorBoundary caught:", error, info.componentStack);
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? <p>Something went wrong.</p>;
}
return this.props.children;
}
}
```
## 3. Retry Pattern
Retry transient failures with exponential backoff and jitter to avoid thundering herd.
```typescript
interface RetryOptions {
maxAttempts?: number;
baseDelayMs?: number;
maxDelayMs?: number;
isRetryable?: (error: unknown) => boolean;
}
async function withRetry<T>(
fn: () => Promise<T>,
options: RetryOptions = {}
): Promise<T> {
const {
maxAttempts = 3,
baseDelayMs = 1000,
maxDelayMs = 30_000,
isRetryable = () => true,
} = options;
let lastError: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt === maxAttempts || !isRetryable(error)) {
throw error;
}
const exponential = baseDelayMs * 2 ** (attempt - 1);
const capped = Math.min(exponential, maxDelayMs);
const jitter = Math.random() * capped;
console.warn(
`Attempt ${attempt}/${maxAttempts} failed, retrying in ${jitter.toFixed(0)}ms`
);
await new Promise((resolve) => setTimeout(resolve, jitter));
}
}
throw lastError;
}
// Usage
const data = await withRetry(
() => fetch("/api/config").then((r) => r.json()),
{
maxAttempts: 3,
baseDelayMs: 500,
isRetryable: (err) =>
err instanceof TypeError || (err as Response)?.status >= 500,
}
);
```
## 4. Graceful Degradation — Feature-Flag Degraded Mode
When a dependency fails, fall back to a degraded but functional state instead of crashing.
```typescript
interface FeatureFlags {
enableRecommendations: boolean;
enableRealTimeUpdates: boolean;
enableAdvancedSearch: boolean;
}
const defaultFlags: FeatureFlags = {
enableRecommendations: true,
enableRealTimeUpdates: true,
enableAdvancedSearch: true,
};
async function getFlags(): Promise<FeatureFlags> {
try {
const resp = await fetch("/api/feature-flags");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return await resp.json();
} catch {
// Fall back to safe defaults when flag service is unavailable
console.warn("Feature flag service unavailable, using defaults");
return defaultFlags;
}
}
// Component that degrades gracefully
function SearchPage() {
const flags = useFeatureFlags();
return (
<div>
<BasicSearch />
{flags.enableAdvancedSearch ? (
<AdvancedFilters />
) : (
<p className="text-sm text-gray-500">
Advanced search is temporarily unavailable.
</p>
)}
</div>
);
}
```
## 5. API Error Responses (Express)
Return consistent, machine-readable error payloads following RFC 7807 Problem Details.
```typescript
import type { Request, Response, NextFunction } from "express";
const STATUS_MAP: Record<ErrorCode, number> = {
NOT_FOUND: 404,
VALIDATION_FAILED: 400,
DUPLICATE_ENTRY: 409,
UNAUTHORIZED: 401,
RATE_LIMITED: 429,
EXTERNAL_SERVICE: 502,
INTERNAL: 500,
};
function errorHandler(
err: Error,
_req: Request,
res: Response,
_next: NextFunction
) {
if (err instanceof AppError) {
const status = STATUS_MAP[err.code] ?? 500;
res.status(status).json({
type: `https://docs.example.com/errors/${err.code.toLowerCase()}`,
title: err.code.replace(/_/g, " ").toLowerCase(),
status,
detail: err.message,
...err.details,
});
return;
}
// Unhandled errors - log full details, return generic message
console.error("Unhandled error:", err);
res.status(500).json({
type: "https://docs.example.com/errors/internal",
title: "Internal Server Error",
status: 500,
detail: "An unexpected error occurred.",
});
}
// Register as the last middleware
app.use(errorHandler);
```
## 6. Error Logging Integration
Attach structured context to errors so they are searchable and actionable in observability tools.
```typescript
interface ErrorContext {
requestId?: string;
userId?: string;
operation?: string;
[key: string]: unknown;
}
function logError(
message: string,
error: unknown,
context: ErrorContext = {}
): void {
const payload: Record<string, unknown> = {
message,
timestamp: new Date().toISOString(),
...context,
};
if (error instanceof AppError) {
payload.errorCode = error.code;
payload.errorMessage = error.message;
payload.errorDetails = error.details;
} else if (error instanceof Error) {
payload.errorType = error.name;
payload.errorMessage = error.message;
payload.stack = error.stack;
} else {
payload.errorRaw = String(error);
}
// Structured JSON log for ingestion by Datadog, CloudWatch, etc.
console.error(JSON.stringify(payload));
}
// Usage
try {
await processOrder(orderId);
} catch (error) {
logError("Order processing failed", error, {
requestId: req.headers["x-request-id"],
operation: "processOrder",
orderId,
});
throw error;
}
```
## 7. Result Pattern
Use a Result type for operations where failure is an expected outcome. Avoids exception overhead and makes the failure path explicit in the type signature.
```typescript
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
// Usage
function parseAge(input: string): Result<number, string> {
const age = Number(input);
if (Number.isNaN(age)) return err(`'${input}' is not a number`);
if (age < 0 || age > 150) return err(`Age ${age} out of range (0-150)`);
return ok(age);
}
function validateRegistration(
data: Record<string, string>
): Result<{ name: string; age: number }, string[]> {
const errors: string[] = [];
const ageResult = parseAge(data.age ?? "");
if (!ageResult.ok) errors.push(ageResult.error);
const name = data.name?.trim() ?? "";
if (!name) errors.push("Name is required");
if (name.length > 100) errors.push("Name must be 100 characters or fewer");
if (errors.length > 0) return err(errors);
return ok({ name, age: (ageResult as { ok: true; value: number }).value });
}
// Caller
const result = validateRegistration(formData);
if (!result.ok) {
return res.status(400).json({ errors: result.error });
}
const user = await createUser(result.value);
```
@@ -1,7 +1,7 @@
---
name: executing-plans
description: >
Trigger this skill whenever there is a written implementation plan ready to execute, or when the user says "execute", "run the plan", "implement the plan", "start building", or references a plan file. Also activate when using subagent-driven development with independent tasks, when the user wants automated execution with quality gates, or when picking up a previously written plan. If a plan document exists and no one is executing it yet, this is the skill to use.
Use when there is a written implementation plan ready to execute, or when the user says "execute", "run the plan", "implement the plan", "start building", or references a plan file. Also activate when using subagent-driven development with independent tasks, when the user wants automated execution with quality gates, or when picking up a previously written plan. If a plan document exists and no one is executing it yet, this is the skill to use.
---
# Executing Plans
@@ -110,7 +110,7 @@ After all tasks complete:
2. Review entire implementation against plan
3. Verify all success criteria met
4. Run full test suite
5. Use `finishing-development-branch` skill
5. Use `finishing-a-development-branch` skill
```
---
@@ -172,6 +172,73 @@ RIGHT: Read plan file, extract task details, then implement
- Any issues encountered
```
### Stack-Specific Task Prompt Examples
**Python/FastAPI:**
```markdown
## Task: Implement GET /api/users endpoint
**Context**: FastAPI + SQLAlchemy async + Pydantic v2
**Files**: src/api/users.py, tests/test_users.py
**Pattern**: Follow src/api/health.py for router setup
**Steps**:
1. Write test: GET /api/users returns 200 with list
2. Verify test fails (404 — route doesn't exist)
3. Implement: APIRouter, async def, Depends(get_db)
4. Verify test passes
5. Add edge case: GET /api/users/999 returns 404 ProblemDetails
**Verify**: pytest tests/test_users.py -v (all green)
```
**TypeScript/NestJS:**
```markdown
## Task: Implement UsersController with CRUD
**Context**: NestJS + Prisma + class-validator DTOs
**Files**: src/users/users.controller.ts, src/users/users.controller.spec.ts
**Pattern**: Follow src/health/ module structure
**Steps**:
1. Write spec: POST /users returns 201 with user
2. Verify spec fails (404 — no route)
3. Implement: Controller, Service, CreateUserDto with @IsEmail()
4. Verify spec passes
5. Add: GET /users/:id returns 404 for missing user
**Verify**: npm test -- --testPathPattern=users.controller (all green)
```
**React/Next.js:**
```markdown
## Task: Build UserTable with sorting and pagination
**Context**: Next.js App Router + TanStack Table + shadcn/ui
**Files**: src/components/user-table.tsx, src/components/user-table.test.tsx
**Pattern**: Follow src/components/data-table.tsx for column defs
**Steps**:
1. Write test: renders table with user data
2. Verify test fails (component doesn't exist)
3. Implement: columns, DataTable wrapper, sort handlers
4. Verify test passes
5. Add test: clicking column header sorts data
**Verify**: npx vitest run src/components/user-table.test.tsx (all green)
```
### Stack-Specific Verification Commands
| Stack | Test Command | Full Verify |
|-------|-------------|-------------|
| Python/FastAPI | `pytest tests/test_<module>.py -v` | `pytest -v && ruff check . && mypy src/` |
| TypeScript/NestJS | `npm test -- --testPathPattern=<module>` | `npm test && npm run lint && npm run build` |
| Next.js | `npx vitest run <file>` | `npm test && next lint && next build` |
### Code Review Subagent Prompt
```markdown
@@ -256,12 +323,12 @@ Before declaring plan execution complete:
- [ ] No Critical issues outstanding
- [ ] No Important issues outstanding
- [ ] Final comprehensive review done
- [ ] Ready for `finishing-development-branch`
- [ ] Ready for `finishing-a-development-branch`
---
## Related Skills
- `methodology/writing-plans` -- Use to create the plan before executing it
- `methodology/dispatching-parallel-agents` -- For coordinating multiple independent agents when plan tasks allow parallelism
- `methodology/verification-before-completion` -- Ensures each task and the final result are properly verified before claiming completion
- `writing-plans` -- Use to create the plan before executing it
- `dispatching-parallel-agents` -- For coordinating multiple independent agents when plan tasks allow parallelism
- `verification-before-completion` -- Ensures each task and the final result are properly verified before claiming completion
@@ -1,7 +1,7 @@
---
name: finishing-development-branch
name: finishing-a-development-branch
description: >
Trigger this skill when implementation is complete and all tests pass, when ready to merge a feature branch, create a PR, or clean up after development. Use whenever you hear "ship it," "ready to merge," "branch is done," or "create a PR." Activate at the end of any feature, bugfix, or chore branch lifecycle to ensure proper verification, option presentation, and worktree cleanup.
Use when implementation is complete and all tests pass, when ready to merge a feature branch, create a PR, or clean up after development. Use whenever you hear "ship it," "ready to merge," "branch is done," or "create a PR." Activate at the end of any feature, bugfix, or chore branch lifecycle to ensure proper verification, option presentation, and worktree cleanup.
---
# Finishing a Development Branch
@@ -270,6 +270,55 @@ Closes #[issue number]
---
## Stack-Specific Pre-Merge Checklist
### Python/FastAPI
```bash
pytest -v --cov=src # Full test suite
ruff check . && ruff format --check . # Lint + format
mypy src/ --strict # Type check
pip-audit # Security audit
alembic upgrade head && alembic check # Verify migrations
```
### TypeScript/NestJS
```bash
npm test # Full test suite
npm run lint # Lint
npm run build # Build (catches type errors)
npm audit --production # Security audit
npx prisma migrate status # Verify migrations
```
### Next.js
```bash
npm test # Tests
next lint # Lint
next build # Build (catches SSR/RSC issues)
```
### Stack-Specific PR Description Extras
**Python/FastAPI PRs** — include:
- Migration included? (alembic revision)
- New dependencies? (requirements.txt changes)
- Async patterns verified? (no blocking calls in async)
**NestJS PRs** — include:
- New modules registered in AppModule?
- DTOs have class-validator decorators?
- Prisma schema changed? (migration included)
**Next.js PRs** — include:
- Server vs Client components correct?
- Bundle size impact?
- `'use client'` directives where needed?
---
## Core Principle
**"Verify tests → Present options → Execute choice → Clean up"**
@@ -284,6 +333,6 @@ Never:
## Related Skills
- `methodology/requesting-code-review` - Use before finishing the branch to get review feedback, especially for Option 2 (Create PR)
- `methodology/verification-before-completion` - Run verification checks before claiming the branch is ready to finish
- `methodology/executing-plans` - If the branch was created from an execution plan, return to the plan to mark tasks complete
- `requesting-code-review` - Use before finishing the branch to get review feedback, especially for Option 2 (Create PR)
- `verification-before-completion` - Run verification checks before claiming the branch is ready to finish
- `executing-plans` - If the branch was created from an execution plan, return to the plan to mark tasks complete
@@ -1,250 +0,0 @@
# Django Patterns Quick Reference
## QuerySet Patterns
### select_related (FK/OneToOne - single JOIN)
```python
# BAD: N+1 queries
for order in Order.objects.all():
print(order.customer.name) # Hits DB each iteration
# GOOD: 1 query with JOIN
for order in Order.objects.select_related("customer"):
print(order.customer.name)
# Chain through multiple FKs
Order.objects.select_related("customer__company")
```
### prefetch_related (M2M/reverse FK - separate query)
```python
# BAD: N+1 on reverse FK
for author in Author.objects.all():
print(author.book_set.all()) # Query per author
# GOOD: 2 queries total
for author in Author.objects.prefetch_related("books"):
print(author.books.all())
# Custom prefetch with filtering
from django.db.models import Prefetch
Author.objects.prefetch_related(
Prefetch("books", queryset=Book.objects.filter(published=True), to_attr="published_books")
)
```
### F Objects (reference model fields in queries)
```python
from django.db.models import F
Product.objects.filter(stock__lt=F("reorder_level")) # Compare fields
Product.objects.filter(id=1).update(stock=F("stock") - 1) # Atomic update
Order.objects.filter(amount__gt=F("customer__credit_limit")) # Across relations
```
### Q Objects (complex lookups with OR/NOT)
```python
from django.db.models import Q
# OR
User.objects.filter(Q(role="admin") | Q(is_superuser=True))
# NOT
User.objects.filter(~Q(status="banned"))
# Complex combinations
User.objects.filter(
(Q(role="admin") | Q(role="staff")) & ~Q(status="inactive")
)
# Dynamic query building
conditions = Q()
if name: conditions &= Q(name__icontains=name)
if email: conditions &= Q(email__icontains=email)
User.objects.filter(conditions)
```
### Subquery and OuterRef
```python
from django.db.models import Subquery, OuterRef, Exists
# Subquery: latest order date per customer
latest_order = Order.objects.filter(
customer=OuterRef("pk")
).order_by("-created_at").values("created_at")[:1]
Customer.objects.annotate(last_order=Subquery(latest_order))
# Exists: customers with orders
Customer.objects.annotate(
has_orders=Exists(Order.objects.filter(customer=OuterRef("pk")))
).filter(has_orders=True)
```
### Aggregation
```python
from django.db.models import Count, Sum, Avg
# Aggregate (returns dict)
Order.objects.aggregate(total=Sum("amount"), avg=Avg("amount"))
# Annotate (per-row)
Customer.objects.annotate(order_count=Count("orders"))
```
---
## Model Patterns
### Abstract Base Model
```python
class TimestampMixin(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True # No DB table created
class Order(TimestampMixin):
amount = models.DecimalField(max_digits=10, decimal_places=2)
# Inherits created_at, updated_at
```
### Proxy Model (same table, different behavior)
```python
class PendingOrderManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(status="pending")
class PendingOrder(Order):
objects = PendingOrderManager()
class Meta:
proxy = True # Same DB table as Order
```
### Custom Manager and QuerySet
```python
class PublishedQuerySet(models.QuerySet):
def published(self):
return self.filter(status="published")
def by_author(self, author):
return self.filter(author=author)
class Article(models.Model):
objects = PublishedQuerySet.as_manager()
# Chainable: Article.objects.published().by_author(user)
```
---
## View Patterns
### Class-Based View Mixins
| Mixin | Purpose |
|-------|---------|
| `LoginRequiredMixin` | Require authentication |
| `PermissionRequiredMixin` | Require specific permission |
| `UserPassesTestMixin` | Custom permission test |
| `FormView` | Handle form display + submission |
| `CreateView` / `UpdateView` | Model form CRUD |
| `ListView` / `DetailView` | Read operations |
```python
class OrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
model = Order
permission_required = "orders.view_order"
paginate_by = 25
def get_queryset(self):
return super().get_queryset().filter(customer=self.request.user)
```
---
## Django REST Framework Patterns
### Nested Serializers
```python
class AddressSerializer(serializers.ModelSerializer):
class Meta:
model = Address
fields = ["street", "city", "zip_code"]
class CustomerSerializer(serializers.ModelSerializer):
address = AddressSerializer()
class Meta:
model = Customer
fields = ["id", "name", "address"]
def create(self, validated_data):
address_data = validated_data.pop("address")
address = Address.objects.create(**address_data)
return Customer.objects.create(address=address, **validated_data)
```
### Custom Permissions
```python
from rest_framework.permissions import BasePermission
class IsOwner(BasePermission):
def has_object_permission(self, request, view, obj):
return obj.owner == request.user
class IsAdminOrReadOnly(BasePermission):
def has_permission(self, request, view):
if request.method in ("GET", "HEAD", "OPTIONS"):
return True
return request.user.is_staff
# Usage
class OrderViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, IsOwner]
```
### ViewSet Actions
```python
from rest_framework.decorators import action
class OrderViewSet(viewsets.ModelViewSet):
queryset = Order.objects.all()
serializer_class = OrderSerializer
@action(detail=True, methods=["post"])
def cancel(self, request, pk=None):
order = self.get_object()
order.cancel()
return Response({"status": "cancelled"})
@action(detail=False, methods=["get"])
def summary(self, request):
return Response(self.get_queryset().aggregate(total=Sum("amount"), count=Count("id")))
```
### Filtering (django-filter)
```python
class OrderFilter(django_filters.FilterSet):
min_amount = django_filters.NumberFilter(field_name="amount", lookup_expr="gte")
max_amount = django_filters.NumberFilter(field_name="amount", lookup_expr="lte")
class Meta:
model = Order
fields = ["status", "customer"]
```
@@ -1,229 +0,0 @@
# FastAPI Project Structure Reference
## Small Project (1-5 endpoints, single module)
```
project/
├── main.py # App factory, routes, startup
├── models.py # Pydantic schemas + SQLAlchemy models
├── database.py # DB connection, session factory
├── config.py # Settings via pydantic-settings
├── requirements.txt
├── .env
└── tests/
├── conftest.py # Fixtures (test client, test DB)
└── test_main.py
```
**When to use**: Prototypes, microservices, internal tools, single-domain APIs.
**`main.py` structure**:
```python
from fastapi import FastAPI
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
# startup
yield
# shutdown
app = FastAPI(lifespan=lifespan)
@app.get("/health")
async def health(): return {"status": "ok"}
```
---
## Medium Project (5-20 endpoints, feature-grouped)
```
project/
├── app/
│ ├── __init__.py
│ ├── main.py # App factory, include routers
│ ├── config.py # Settings (pydantic-settings)
│ ├── database.py # Engine, SessionLocal, Base
│ ├── dependencies.py # Shared deps (get_db, get_current_user)
│ ├── exceptions.py # Custom exception handlers
│ ├── middleware.py # CORS, logging, timing middleware
│ │
│ ├── auth/
│ │ ├── __init__.py
│ │ ├── router.py # POST /login, POST /register
│ │ ├── schemas.py # LoginRequest, TokenResponse
│ │ ├── models.py # User SQLAlchemy model
│ │ ├── service.py # Business logic (hash, verify, tokens)
│ │ └── dependencies.py # get_current_user, require_role
│ │
│ ├── items/
│ │ ├── __init__.py
│ │ ├── router.py # CRUD endpoints
│ │ ├── schemas.py # ItemCreate, ItemRead, ItemUpdate
│ │ ├── models.py # Item SQLAlchemy model
│ │ └── service.py # Business logic
│ │
│ └── shared/
│ ├── __init__.py
│ ├── pagination.py # Pagination params + response schema
│ └── filters.py # Common query filter patterns
├── alembic/ # DB migrations
│ ├── env.py
│ └── versions/
├── alembic.ini
├── requirements.txt
├── pyproject.toml
├── Dockerfile
├── docker-compose.yml
└── tests/
├── conftest.py
├── auth/
│ └── test_router.py
└── items/
├── test_router.py
└── test_service.py
```
**When to use**: Multi-feature APIs, team projects, typical SaaS backends.
**Key patterns**:
- Each feature gets its own directory with router, schemas, models, service
- `router.py` uses `APIRouter(prefix="/items", tags=["items"])`
- `main.py` includes routers: `app.include_router(items.router)`
- Shared deps in root `dependencies.py`, feature-specific in feature dir
---
## Large Project (20+ endpoints, domain-driven)
```
project/
├── src/
│ ├── __init__.py
│ ├── main.py # App factory only
│ ├── config.py # Layered settings
│ │
│ ├── core/ # Framework-level concerns
│ │ ├── __init__.py
│ │ ├── database.py # Engine, session management
│ │ ├── security.py # JWT, hashing, RBAC
│ │ ├── exceptions.py # Base exceptions + handlers
│ │ ├── middleware.py # All middleware stack
│ │ ├── dependencies.py # Cross-cutting deps
│ │ ├── events.py # Domain event bus
│ │ └── pagination.py # Cursor + offset pagination
│ │
│ ├── domain/ # Business logic (framework-agnostic)
│ │ ├── users/
│ │ │ ├── __init__.py
│ │ │ ├── entity.py # Domain entity (plain dataclass)
│ │ │ ├── repository.py # Abstract repository interface
│ │ │ ├── service.py # Business rules
│ │ │ └── events.py # Domain events
│ │ ├── orders/
│ │ │ └── ...
│ │ └── payments/
│ │ └── ...
│ │
│ ├── infrastructure/ # External system adapters
│ │ ├── database/
│ │ │ ├── models.py # All SQLAlchemy models
│ │ │ ├── repositories/ # Concrete repo implementations
│ │ │ │ ├── user_repo.py
│ │ │ │ └── order_repo.py
│ │ │ └── migrations/ # Alembic
│ │ ├── cache/
│ │ │ └── redis_client.py
│ │ ├── email/
│ │ │ └── smtp_service.py
│ │ └── external/
│ │ └── stripe_client.py
│ │
│ └── api/ # HTTP layer only
│ ├── __init__.py
│ ├── v1/
│ │ ├── __init__.py # v1 router aggregator
│ │ ├── users.py # Thin: parse request -> call service -> format response
│ │ ├── orders.py
│ │ └── payments.py
│ ├── v2/
│ │ └── ...
│ ├── schemas/ # Request/response schemas
│ │ ├── user_schemas.py
│ │ ├── order_schemas.py
│ │ └── common.py
│ ├── dependencies.py # API-layer deps
│ └── websockets/
│ └── notifications.py
├── tests/
│ ├── conftest.py
│ ├── unit/
│ │ ├── domain/
│ │ │ └── test_user_service.py
│ │ └── ...
│ ├── integration/
│ │ ├── test_user_api.py
│ │ └── test_order_flow.py
│ └── e2e/
│ └── test_checkout.py
├── scripts/ # Dev/ops scripts
│ ├── seed_db.py
│ └── migrate.py
├── pyproject.toml
├── Dockerfile
├── docker-compose.yml
└── Makefile
```
**When to use**: Complex domains, multiple teams, long-lived products.
**Key patterns**:
- **Domain layer** has zero framework imports (testable in isolation)
- **Infrastructure** implements domain interfaces (repository pattern)
- **API layer** is thin: validation, auth, call service, return schema
- API versioning via `/api/v1/`, `/api/v2/`
- Separate unit, integration, and e2e test directories
---
## File Responsibilities
| File | Responsibility | Dependencies |
|------|---------------|-------------|
| `router.py` | HTTP handling, request parsing, response formatting | schemas, service, dependencies |
| `schemas.py` | Pydantic models for request/response validation | None (or shared schemas) |
| `models.py` | SQLAlchemy/ODM models (DB table mapping) | database |
| `service.py` | Business logic, orchestration | repository/models, external services |
| `dependencies.py` | FastAPI `Depends()` callables | database, config, auth |
| `exceptions.py` | Custom exceptions + handlers | None |
| `config.py` | `BaseSettings` with env loading | None |
## Router Registration Pattern
```python
# app/main.py
from fastapi import FastAPI
from app.auth.router import router as auth_router
from app.items.router import router as items_router
def create_app() -> FastAPI:
app = FastAPI(title="My API")
app.include_router(auth_router)
app.include_router(items_router)
return app
app = create_app()
```
```python
# app/items/router.py
from fastapi import APIRouter, Depends
router = APIRouter(prefix="/items", tags=["items"])
@router.get("/")
async def list_items(db=Depends(get_db)): ...
```
@@ -1,237 +0,0 @@
# Next.js Patterns Quick Reference
> App Router (Next.js 13.4+). Pages Router patterns not covered.
## App Router File Conventions
| File | Purpose | Renders |
|------|---------|---------|
| `page.tsx` | Route UI (makes route publicly accessible) | Server Component |
| `layout.tsx` | Shared UI wrapping children (preserved on nav) | Server Component |
| `template.tsx` | Like layout but re-mounts on navigation | Server Component |
| `loading.tsx` | Instant loading UI (Suspense boundary) | Server Component |
| `error.tsx` | Error boundary for segment | **Client Component** |
| `not-found.tsx` | UI for `notFound()` calls | Server Component |
| `route.ts` | API endpoint (GET, POST, etc.) | N/A |
| `default.tsx` | Fallback for parallel routes | Server Component |
| `middleware.ts` | Runs before requests (root only) | Edge Runtime |
| `opengraph-image.tsx` | Dynamic OG image generation | Edge Runtime |
### Route Segment Folders
| Pattern | Example | Purpose |
|---------|---------|---------|
| Static | `app/about/page.tsx` | `/about` |
| Dynamic | `app/blog/[slug]/page.tsx` | `/blog/hello-world` |
| Catch-all | `app/docs/[...slug]/page.tsx` | `/docs/a/b/c` |
| Optional catch-all | `app/docs/[[...slug]]/page.tsx` | `/docs` or `/docs/a/b` |
| Route group | `app/(marketing)/about/page.tsx` | Groups without URL segment |
| Parallel route | `app/@modal/login/page.tsx` | Simultaneous route slots |
| Intercepted route | `app/(.)photo/[id]/page.tsx` | Intercept navigation |
---
## Caching Layers Summary
| Layer | What | Where | Duration | Opt-out |
|-------|------|-------|----------|---------|
| Request Memoization | `fetch()` dedup in single render | Server | Per request | `AbortController` |
| Data Cache | `fetch()` results | Server | Persistent | `cache: 'no-store'` or `revalidate: 0` |
| Full Route Cache | Static HTML + RSC payload | Server | Persistent | Dynamic functions or `revalidate` |
| Router Cache | RSC payload | Client | Session (30s dynamic, 5min static) | `router.refresh()` |
### Revalidation Strategies
```typescript
// Time-based
fetch(url, { next: { revalidate: 60 } }); // Revalidate every 60s
// On-demand (from API route or Server Action)
import { revalidatePath, revalidateTag } from "next/cache";
revalidatePath("/blog"); // Revalidate path
revalidateTag("posts"); // Revalidate by tag
// Tag a fetch for on-demand revalidation
fetch(url, { next: { tags: ["posts"] } });
// Opt out entirely
fetch(url, { cache: "no-store" });
// Route segment config
export const dynamic = "force-dynamic"; // Always dynamic
export const revalidate = 60; // Segment-level ISR
```
---
## Data Fetching Patterns
### Server Component (default, preferred)
```typescript
// Async Server Component - fetch directly
async function BlogPage() {
const posts = await fetch("https://api.example.com/posts", {
next: { revalidate: 3600 }
}).then(r => r.json());
return <PostList posts={posts} />;
}
```
### Parallel Data Fetching
```typescript
async function Dashboard() {
// Start all fetches simultaneously
const [user, orders, stats] = await Promise.all([
getUser(),
getOrders(),
getStats(),
]);
return <DashboardView user={user} orders={orders} stats={stats} />;
}
```
### Streaming with Suspense
```typescript
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
{/* Shows instantly */}
<StaticContent />
{/* Streams in when ready */}
<Suspense fallback={<Skeleton />}>
<SlowDataComponent />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<AnotherSlowComponent />
</Suspense>
</div>
);
}
```
### Cached Server Functions
```typescript
import { cache } from "react";
// Deduplicated across components in same request
export const getUser = cache(async (id: string) => {
const res = await fetch(`/api/users/${id}`);
return res.json();
});
```
---
## Server Action Patterns
### Basic Form Action
```typescript
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const body = formData.get("body") as string;
await db.post.create({ data: { title, body } });
revalidatePath("/posts");
redirect("/posts");
}
```
```typescript
// In a Server Component
import { createPost } from "./actions";
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" />
<textarea name="body" />
<button type="submit">Create</button>
</form>
);
}
```
### Optimistic Updates
```typescript
"use client";
import { useOptimistic } from "react";
function TodoList({ todos, addTodo }) {
const [optimisticTodos, addOptimistic] = useOptimistic(
todos,
(state, newTodo: string) => [...state, { text: newTodo, pending: true }]
);
return (
<form action={async (formData) => {
const text = formData.get("text") as string;
addOptimistic(text);
await addTodo(text);
}}>
{optimisticTodos.map(todo => (
<div style={{ opacity: todo.pending ? 0.5 : 1 }}>{todo.text}</div>
))}
<input name="text" />
</form>
);
}
```
---
## Middleware Patterns
```typescript
// middleware.ts (project root)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
// Auth check
const token = request.cookies.get("token")?.value;
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
// Add headers
const response = NextResponse.next();
response.headers.set("x-request-id", crypto.randomUUID());
return response;
}
// Only run on matching paths
export const config = {
matcher: [
// Match all except static files and api
"/((?!_next/static|_next/image|favicon.ico|api/).*)",
],
};
```
### Common Middleware Uses
| Use Case | Pattern |
|----------|---------|
| Auth redirect | Check cookie/header, redirect to login |
| i18n routing | Read Accept-Language, redirect to locale |
| A/B testing | Set cookie, rewrite to variant |
| Rate limiting | Check IP/token bucket, return 429 |
| Bot protection | Check User-Agent, block or challenge |
| Feature flags | Read flag, rewrite to feature variant |
@@ -1,243 +0,0 @@
# React Patterns Quick Reference
## Hook Rules
1. Only call hooks at the **top level** (not inside loops, conditions, or nested functions)
2. Only call hooks from **React function components** or **custom hooks**
3. Custom hooks **must** start with `use`
4. Hook call order must be **identical** on every render
```typescript
// BAD
if (isLoggedIn) {
const [user, setUser] = useState(null); // Conditional hook call
}
// GOOD
const [user, setUser] = useState(null);
// Use the state conditionally instead
```
---
## Custom Hook Recipes
### useLocalStorage
```typescript
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
// Usage
const [theme, setTheme] = useLocalStorage("theme", "dark");
```
### useDebounce
```typescript
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// Usage
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search, 300);
useEffect(() => {
fetchResults(debouncedSearch);
}, [debouncedSearch]);
```
### useMediaQuery
```typescript
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(
() => window.matchMedia(query).matches
);
useEffect(() => {
const mql = window.matchMedia(query);
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
mql.addEventListener("change", handler);
return () => mql.removeEventListener("change", handler);
}, [query]);
return matches;
}
// Usage
const isMobile = useMediaQuery("(max-width: 768px)");
```
### usePrevious
```typescript
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
});
return ref.current;
}
// Usage
const prevCount = usePrevious(count);
```
---
## Compound Component Pattern
```typescript
// Components that work together sharing implicit state
const Accordion = ({ children }: { children: React.ReactNode }) => {
const [openIndex, setOpenIndex] = useState<number | null>(null);
return (
<AccordionContext.Provider value={{ openIndex, setOpenIndex }}>
<div role="tablist">{children}</div>
</AccordionContext.Provider>
);
};
const AccordionItem = ({ index, title, children }: {
index: number; title: string; children: React.ReactNode;
}) => {
const { openIndex, setOpenIndex } = useContext(AccordionContext);
const isOpen = openIndex === index;
return (
<div>
<button onClick={() => setOpenIndex(isOpen ? null : index)}>
{title}
</button>
{isOpen && <div>{children}</div>}
</div>
);
};
Accordion.Item = AccordionItem;
// Usage - clean, declarative API
<Accordion>
<Accordion.Item index={0} title="Section 1">Content 1</Accordion.Item>
<Accordion.Item index={1} title="Section 2">Content 2</Accordion.Item>
</Accordion>
```
---
## Context Patterns
### Split State and Dispatch
```typescript
// Prevent unnecessary re-renders by splitting contexts
const StateContext = createContext<State | null>(null);
const DispatchContext = createContext<Dispatch | null>(null);
function Provider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>
{children}
</StateContext.Provider>
</DispatchContext.Provider>
);
}
// Components that only dispatch won't re-render on state changes
function useAppDispatch() {
const ctx = useContext(DispatchContext);
if (!ctx) throw new Error("Must be used within Provider");
return ctx;
}
```
---
## Performance Checklist
### When to Memoize
| Situation | Solution |
|-----------|----------|
| Expensive computation | `useMemo(() => compute(data), [data])` |
| Stable callback for child | `useCallback(fn, [deps])` |
| Prevent child re-render | `React.memo(Component)` |
| Stable object/array prop | `useMemo(() => ({ ... }), [deps])` |
### When NOT to Memoize
- Simple/cheap calculations
- Primitives (strings, numbers, booleans) -- already stable
- Functions only used in the same component
- Components that are cheap to render
### Key Optimizations
```typescript
// 1. Move state down (colocate state with where it's used)
// BAD: parent re-renders everything
function Parent() {
const [hover, setHover] = useState(false);
return <><ExpensiveChild /><HoverTarget onHover={setHover} /></>;
}
// GOOD: isolate the stateful part
function Parent() {
return <><ExpensiveChild /><HoverSection /></>;
}
function HoverSection() {
const [hover, setHover] = useState(false);
return <HoverTarget onHover={setHover} />;
}
// 2. Pass children to avoid re-rendering them
function ScrollTracker({ children }: { children: React.ReactNode }) {
const [scroll, setScroll] = useState(0);
// children are created by parent, not re-created on scroll change
return <div onScroll={e => setScroll(e.currentTarget.scrollTop)}>{children}</div>;
}
// 3. Use key to reset component state
<Form key={selectedId} initialData={data} />
// 4. Lazy load heavy components
const HeavyChart = lazy(() => import("./HeavyChart"));
<Suspense fallback={<Skeleton />}>
<HeavyChart data={data} />
</Suspense>
```
### React DevTools Profiler Checklist
1. Enable "Highlight updates" to spot unnecessary re-renders
2. Record a profiler session and look for:
- Components re-rendering without visible changes
- Long render times (>16ms blocks frames)
- Cascading re-renders from context changes
3. Fix with: state colocation, memo, context splitting, or external state
+63
View File
@@ -0,0 +1,63 @@
---
name: frontend-styling
description: >
Use when styling web components with Tailwind CSS or ensuring accessibility compliance — including utility classes, responsive breakpoints, dark mode, WCAG, ARIA, aria-label, aria-describedby, screen reader, keyboard navigation, focus management, color contrast, alt text, semantic HTML, or skip links.
---
# Frontend Styling
## When to Use
- Styling React/Next.js components with Tailwind CSS utility classes
- Building responsive layouts, dark mode support, or design systems
- Ensuring WCAG 2.1 AA compliance for UI components
- Adding keyboard navigation, focus management, or screen reader support
- Fixing accessibility audit findings
## When NOT to Use
- Backend API development with no UI surface
- Component logic or state management — use `frontend`
- CLI tools (different accessibility model)
---
## Quick Reference
| Topic | Reference | Key features |
|-------|-----------|-------------|
| Tailwind CSS | `references/tailwind.md` | Utility classes, responsive, dark mode, cn(), twMerge, @apply |
| Accessibility | `references/accessibility.md` | WCAG, ARIA, keyboard nav, focus trapping, semantic HTML, alt text |
---
## Best Practices
1. **Mobile-first always.** Write base styles for mobile, layer breakpoint prefixes for larger screens.
2. **Use the spacing scale consistently.** Stick to Tailwind's default scale rather than arbitrary values.
3. **Extract repeated patterns to components** when the same classes appear three or more times.
4. **Prefer `cn()` / `twMerge` for conditional classes** to avoid class conflicts.
5. **Use CSS variables for theme tokens.**
6. **Use semantic HTML elements**`button`, `a`, `input` instead of `div` and `span` for interactive elements.
7. **Every `<img>` needs `alt`.** Decorative images use `alt=""`.
8. **Never use `tabIndex > 0`.** It breaks natural tab order.
## Common Pitfalls
1. **Dynamic class name construction**`bg-${color}-500` will not work with Tailwind's JIT compiler.
2. **Forgetting content paths in `tailwind.config.js`.**
3. **Class conflicts without twMerge.**
4. **Ignoring dark mode from the start.**
5. **`div` and `span` for interactive elements** — use semantic HTML instead.
6. **Missing `alt` text on images.**
7. **Unlabeled form inputs.**
8. **Focus not managed in SPAs** — especially modals, drawers, and dropdown menus.
9. **`aria-hidden="true"` on focusable elements.**
10. **Color-only error indicators** — always include text or icons alongside color changes.
---
## Related Skills
- `frontend` — React and Next.js component patterns
- `owasp` — Security aspects of frontend development
@@ -0,0 +1,316 @@
# Frontend Styling — Accessibility Patterns
# Accessibility (a11y)
## When to Use
- Building new UI components (buttons, modals, forms, navigation)
- Reviewing existing components for WCAG 2.1 AA compliance
- Adding keyboard navigation to interactive elements
- Implementing focus management (modals, drawers, dropdown menus)
- Fixing accessibility audit findings
## When NOT to Use
- Backend API development (no UI surface)
- CLI tools (different accessibility model)
- Internal admin tools where the team explicitly opts out (document the decision)
---
## Core Principles
### Semantic HTML first
Use the right element before reaching for ARIA:
```tsx
// BAD — div pretending to be a button
<div onClick={handleClick} className="btn">Submit</div>
// GOOD — semantic button
<button onClick={handleClick} type="submit">Submit</button>
```
```tsx
// BAD — div pretending to be a nav
<div className="nav">
<div onClick={() => navigate('/home')}>Home</div>
</div>
// GOOD — semantic nav + links
<nav aria-label="Main navigation">
<a href="/home">Home</a>
<a href="/about">About</a>
</nav>
```
### The first rule of ARIA
**"No ARIA is better than bad ARIA."** Only use ARIA when native HTML semantics can't express the relationship.
---
## Interactive Components
### Buttons and links
```tsx
// Button — performs an action
<button type="button" onClick={onDelete}>
Delete Item
</button>
// Link — navigates somewhere
<a href="/settings">Settings</a>
// Icon-only button — MUST have accessible name
<button type="button" aria-label="Close dialog" onClick={onClose}>
<XIcon aria-hidden="true" />
</button>
```
### Forms
```tsx
// Every input needs a label
<div>
<label htmlFor="email">Email address</label>
<input
id="email"
type="email"
aria-required="true"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<p id="email-error" role="alert">
{errors.email.message}
</p>
)}
</div>
```
```tsx
// Form with react-hook-form + shadcn/ui
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} type="email" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
```
### Modals / Dialogs
```tsx
// Using shadcn/ui Dialog (Radix-based — accessibility built in)
<Dialog>
<DialogTrigger asChild>
<Button>Open Settings</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Settings</DialogTitle>
<DialogDescription>Update your preferences below.</DialogDescription>
</DialogHeader>
{/* Focus is automatically trapped inside */}
<form>...</form>
</DialogContent>
</Dialog>
```
For custom modals without Radix:
```tsx
// Focus trap + escape key + scroll lock
function Modal({ isOpen, onClose, title, children }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isOpen) return;
const el = ref.current;
const previousFocus = document.activeElement as HTMLElement;
// Focus first focusable element
el?.querySelector<HTMLElement>('button, [href], input, select, textarea')?.focus();
// Trap focus
function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') onClose();
if (e.key !== 'Tab') return;
// ... focus trap logic
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
previousFocus?.focus(); // Restore focus on close
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div role="dialog" aria-modal="true" aria-labelledby="modal-title" ref={ref}>
<h2 id="modal-title">{title}</h2>
{children}
</div>
);
}
```
---
## Keyboard Navigation
### Required keyboard support
| Component | Keys |
|-----------|------|
| Button | `Enter`, `Space` to activate |
| Link | `Enter` to navigate |
| Modal | `Escape` to close, `Tab` to cycle focus |
| Dropdown | `Arrow Up/Down` to navigate, `Enter` to select, `Escape` to close |
| Tabs | `Arrow Left/Right` to switch, `Tab` to enter content |
| Checkbox | `Space` to toggle |
### Skip link
```tsx
// First element in <body> — lets keyboard users skip navigation
<a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:p-4 focus:bg-white focus:z-50">
Skip to main content
</a>
// ... navigation ...
<main id="main-content">
{/* Page content */}
</main>
```
### Focus visible
```css
/* Tailwind — ensure focus ring is visible */
@layer base {
*:focus-visible {
@apply outline-2 outline-offset-2 outline-blue-600;
}
}
```
---
## Color and Contrast
### WCAG AA contrast ratios
| Text size | Minimum ratio |
|-----------|--------------|
| Normal text (<18px) | 4.5:1 |
| Large text (>=18px bold or >=24px) | 3:1 |
| UI components & graphics | 3:1 |
### Don't rely on color alone
```tsx
// BAD — only color indicates error
<input className={error ? 'border-red-500' : 'border-gray-300'} />
// GOOD — color + icon + text
<input
className={error ? 'border-red-500' : 'border-gray-300'}
aria-invalid={!!error}
aria-describedby={error ? 'email-error' : undefined}
/>
{error && (
<p id="email-error" role="alert" className="text-red-600 flex items-center gap-1">
<AlertIcon aria-hidden="true" /> {error}
</p>
)}
```
---
## Images and Media
```tsx
// Informative image — describe the content
<img src="/chart.png" alt="Revenue grew 25% from Q1 to Q2 2026" />
// Decorative image -- hide from screen readers
<img src="/decoration.svg" alt="" aria-hidden="true" />
// Complex image — link to full description
<figure>
<img src="/architecture.png" alt="System architecture diagram" aria-describedby="arch-desc" />
<figcaption id="arch-desc">
Three-tier architecture: React frontend, FastAPI backend, PostgreSQL database.
</figcaption>
</figure>
```
---
## Testing
### Automated
```bash
# axe-core via Playwright
npx playwright test --project=accessibility
# eslint-plugin-jsx-a11y (catches common issues at lint time)
npm install -D eslint-plugin-jsx-a11y
```
```typescript
// Playwright a11y test
import AxeBuilder from '@axe-core/playwright';
test('homepage has no a11y violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
```
### Manual checklist
- [ ] Tab through entire page — can you reach and operate every interactive element?
- [ ] Use screen reader (VoiceOver / NVDA) — does every element have an accessible name?
- [ ] Zoom to 200% — does layout remain usable?
- [ ] Disable CSS — does content order make sense?
- [ ] Check color contrast with browser DevTools
---
## Common Pitfalls
1. **`div` and `span` for interactive elements.** Use `button`, `a`, `input` instead. Divs have no keyboard support or ARIA roles by default.
2. **Missing `alt` text on images.** Every `<img>` needs `alt`. Decorative images use `alt=""`.
3. **Unlabeled form inputs.** Every input needs a `<label>` with matching `htmlFor`/`id`.
4. **Focus not managed in SPAs.** When navigating to a new page in React/Next.js, move focus to the main content area.
5. **`tabIndex > 0`.** Never use positive `tabIndex`. It breaks natural tab order. Use `0` (natural order) or `-1` (programmatic focus only).
6. **`aria-hidden="true"` on focusable elements.** Hidden elements that can receive focus confuse screen readers.
7. **Color-only error indicators.** Always pair color with text, icons, or patterns.
---
## Related Skills
- `react` — Component patterns that naturally support accessibility
- `shadcn-ui` — Radix-based components with built-in a11y
- `tailwind` — Utility classes for focus styles and screen-reader-only text
- `playwright` — E2E testing with axe-core accessibility checks
- `nextjs` — App Router patterns for accessible page transitions
@@ -1,8 +1,5 @@
---
name: tailwind
description: >
Use this skill when styling web components with Tailwind CSS utility classes, building responsive layouts, or implementing dark mode support. Trigger on keywords like Tailwind, utility classes, responsive breakpoints, dark mode, flex, grid layout, and mobile-first design. Also apply when creating component variants with Tailwind, configuring tailwind.config, or migrating from traditional CSS to a utility-first approach.
---
# Frontend Styling — Tailwind CSS Patterns
# Tailwind CSS
@@ -583,6 +580,6 @@ export function cn(...inputs: ClassValue[]) {
## Related Skills
- `frontend/shadcn-ui` - Component library built on Radix primitives with Tailwind styling
- `frameworks/react` - React component patterns and best practices
- `frameworks/nextjs` - Next.js framework with built-in Tailwind support
- `shadcn-ui` - Component library built on Radix primitives with Tailwind styling
- `react` - React component patterns and best practices
- `nextjs` - Next.js framework with built-in Tailwind support
+66
View File
@@ -0,0 +1,66 @@
---
name: frontend
description: >
Use when building React components, Next.js applications, or shadcn/ui interfaces — including hooks, useState, useEffect, useCallback, useMemo, Server Components, App Router, Server Actions, SSR, SSG, ISR, Radix primitives, cn() utility, next/navigation, or component architecture.
---
# Frontend
## When to Use
- Building React components, custom hooks, or managing component state
- Next.js App Router, Server Components, Server Actions, route handlers
- shadcn/ui components with Radix primitives and react-hook-form
- Client-side interactivity, context providers, or component composition
- SEO-critical sites needing SSR/SSG/ISR
## When NOT to Use
- Styling and accessibility — use `frontend-styling`
- Backend API development — use `backend-frameworks`
- State management architecture decisions — use `state-management`
---
## Quick Reference
| Topic | Reference | Key features |
|-------|-----------|-------------|
| React | `references/react.md` | Hooks, memo, context, composition, effect cleanup, custom hooks |
| Next.js | `references/nextjs.md` | App Router, Server Components, Server Actions, loading.tsx, middleware |
| shadcn/ui | `references/shadcn-ui.md` | Radix primitives, cn(), asChild, CSS variables, Zod forms |
---
## Best Practices
1. **Keep components small and single-purpose** (~100 lines max).
2. **Use TypeScript interfaces for all props.** Avoid `any`.
3. **Clean up all effects.** Return cleanup from every `useEffect` that subscribes to events or starts timers.
4. **Derive state instead of syncing it.** Compute from props/state during render or with `useMemo`.
5. **Default to Server Components** — only add `"use client"` when you need interactivity (Next.js).
6. **Colocate data fetching with the component that uses it** (Next.js).
7. **Use `loading.tsx` for instant loading states** (Next.js).
8. **Validate Server Action inputs** — Server Actions are public HTTP endpoints (Next.js).
9. **Install shadcn/ui components individually** — only add what you need.
10. **Use `cn()` for all conditional styling** (shadcn/ui).
11. **Keep forms type-safe end to end** — Zod schema + inferred type + `useForm<T>` (shadcn/ui).
## Common Pitfalls
1. **Missing or wrong dependency arrays** in hooks.
2. **Setting state during render** — causes infinite loops.
3. **Using `useEffect` for derived state** — causes double renders; compute inline instead.
4. **Using hooks in Server Components** (Next.js).
5. **Large client bundles from misplaced `"use client"`** (Next.js).
6. **Stale data from aggressive caching** (Next.js).
7. **Forgetting `"use client"` for shadcn/ui components in Next.js App Router.**
8. **Hardcoded colors instead of CSS variables** (shadcn/ui).
---
## Related Skills
- `frontend-styling` — Tailwind CSS and accessibility
- `state-management` — State architecture decisions
- `error-handling` — Error boundaries in React
@@ -1,8 +1,5 @@
---
name: nextjs
description: >
Use this skill when working with Next.js applications, App Router, Server Components, or Server Actions. Trigger for any mention of Next.js, next/server, next/navigation, route handlers, SSR, SSG, ISR, middleware, or the app/ directory structure. Also applies when building full-stack React applications with API routes, implementing streaming or suspense boundaries, or configuring next.config.
---
# Frontend — Next.js Patterns
# Next.js
@@ -15,9 +12,9 @@ description: >
## When NOT to Use
- Pure React SPAs without SSR needs — use the `frameworks/react` skill instead
- Pure React SPAs without SSR needs — use the `react` skill instead
- Non-React frameworks (Vue, Svelte, Angular) — this skill is React/Next.js specific
- Backend-only projects without a frontend — consider `frameworks/fastapi` or `frameworks/django`
- Backend-only projects without a frontend — consider `fastapi` or `django`
---
@@ -683,10 +680,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
## Related Skills
- `frameworks/react` — React component patterns, hooks, and state management
- `languages/typescript` — TypeScript strict mode and type patterns
- `frontend/tailwind` — Styling with Tailwind CSS
- `frontend/shadcn-ui` — UI component library built on Radix and Tailwind
- `patterns/authentication` — Protected routes and auth middleware for Next.js
- `patterns/caching` — Next.js caching layers and invalidation
- `patterns/state-management` — React state management in Next.js apps
- `react` — React component patterns, hooks, and state management
- `typescript` — TypeScript strict mode and type patterns
- `tailwind` — Styling with Tailwind CSS
- `shadcn-ui` — UI component library built on Radix and Tailwind
- `authentication` — Protected routes and auth middleware for Next.js
- `caching` — Next.js caching layers and invalidation
- `state-management` — React state management in Next.js apps
@@ -1,8 +1,5 @@
---
name: react
description: >
Use this skill when building React components, using React hooks, or managing component state. Trigger for any mention of React, JSX, TSX, useState, useEffect, useCallback, useMemo, useContext, custom hooks, or React component patterns. Also applies when implementing context providers, handling component lifecycle, optimizing re-renders, or structuring React application architecture.
---
# Frontend — React Patterns
# React
@@ -707,9 +704,9 @@ function VirtualList({ items }: { items: Item[] }) {
## Related Skills
- `frameworks/nextjs` — Next.js App Router, SSR, and full-stack React patterns
- `languages/typescript` — TypeScript strict mode and type patterns
- `frontend/tailwind` — Styling with Tailwind CSS
- `frontend/shadcn-ui` — UI component library built on Radix and Tailwind
- `testing/vitest` — Testing React components with vitest and testing-library
- `patterns/state-management` — State management patterns for React
- `nextjs` — Next.js App Router, SSR, and full-stack React patterns
- `typescript` — TypeScript strict mode and type patterns
- `tailwind` — Styling with Tailwind CSS
- `shadcn-ui` — UI component library built on Radix and Tailwind
- `vitest` — Testing React components with vitest and testing-library
- `state-management` — State management patterns for React
@@ -1,8 +1,5 @@
---
name: shadcn-ui
description: >
Use this skill when building accessible React UI components with shadcn/ui and Radix primitives. Trigger on keywords like shadcn, Radix, component library, Dialog, Form, Button variants, cn() utility, and asChild composition. Also apply when setting up a design system based on shadcn/ui, customizing component themes via CSS variables, or integrating shadcn/ui forms with react-hook-form and Zod validation.
---
# Frontend — shadcn/ui Patterns
# shadcn/ui
@@ -933,6 +930,6 @@ export function ThemeToggle() {
## Related Skills
- `frontend/tailwind` - Tailwind CSS utility classes used for styling shadcn/ui components
- `frameworks/react` - React patterns and hooks used alongside shadcn/ui
- `frameworks/nextjs` - Next.js integration with shadcn/ui for full-stack applications
- `tailwind` - Tailwind CSS utility classes used for styling shadcn/ui components
- `react` - React patterns and hooks used alongside shadcn/ui
- `nextjs` - Next.js integration with shadcn/ui for full-stack applications
@@ -1,242 +0,0 @@
# shadcn/ui Component Recipes
Common compositions using shadcn/ui. Install components via `npx shadcn@latest add`.
---
## Search Command Palette
```bash
npx shadcn@latest add command dialog
```
```tsx
import { useState, useEffect } from "react";
import {
CommandDialog, CommandEmpty, CommandGroup,
CommandInput, CommandItem, CommandList,
} from "@/components/ui/command";
const items = [
{ group: "Pages", entries: [
{ label: "Dashboard", href: "/dashboard" },
{ label: "Settings", href: "/settings" },
]},
{ group: "Actions", entries: [
{ label: "Create project", action: "new-project" },
]},
];
export function CommandPalette() {
const [open, setOpen] = useState(false);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((prev) => !prev);
}
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, []);
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
{items.map((group) => (
<CommandGroup key={group.group} heading={group.group}>
{group.entries.map((entry) => (
<CommandItem key={entry.label} onSelect={() => setOpen(false)}>
{entry.label}
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</CommandDialog>
);
}
```
---
## Settings Form
`npx shadcn@latest add form input switch button separator`
```tsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Separator } from "@/components/ui/separator";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
const schema = z.object({ displayName: z.string().min(2).max(50), email: z.string().email(), notifications: z.boolean() });
export function SettingsForm() {
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues: { displayName: "", email: "", notifications: true },
});
return (
<div className="max-w-2xl space-y-6">
<div>
<h2 className="text-lg font-semibold">Settings</h2>
<p className="text-sm text-muted-foreground">Manage your account preferences.</p>
</div>
<Separator />
<Form {...form}>
<form onSubmit={form.handleSubmit(console.log)} className="space-y-6">
<FormField control={form.control} name="displayName" render={({ field }) => (
<FormItem><FormLabel>Display Name</FormLabel><FormControl><Input placeholder="Jane Doe" {...field} /></FormControl><FormMessage /></FormItem>
)} />
<FormField control={form.control} name="email" render={({ field }) => (
<FormItem><FormLabel>Email</FormLabel><FormControl><Input type="email" {...field} /></FormControl><FormMessage /></FormItem>
)} />
<FormField control={form.control} name="notifications" render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div><FormLabel>Notifications</FormLabel><FormDescription>Receive account emails.</FormDescription></div>
<FormControl><Switch checked={field.value} onCheckedChange={field.onChange} /></FormControl>
</FormItem>
)} />
<Button type="submit">Save changes</Button>
</form>
</Form>
</div>
);
}
```
---
## Data Table with Filters
`npx shadcn@latest add table input button badge dropdown-menu`
```tsx
import { useState, useMemo } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
interface User { id: string; name: string; email: string; role: "admin" | "user"; status: "active" | "inactive"; }
export function UsersTable({ users }: { users: User[] }) {
const [search, setSearch] = useState("");
const [roleFilter, setRoleFilter] = useState<string | null>(null);
const filtered = useMemo(() => users.filter((u) => {
const match = u.name.toLowerCase().includes(search.toLowerCase()) || u.email.toLowerCase().includes(search.toLowerCase());
return match && (!roleFilter || u.role === roleFilter);
}), [users, search, roleFilter]);
return (
<div className="space-y-4">
<div className="flex items-center gap-3">
<Input placeholder="Search..." value={search} onChange={(e) => setSearch(e.target.value)} className="max-w-sm" />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">{roleFilter ?? "All roles"}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => setRoleFilter(null)}>All</DropdownMenuItem>
<DropdownMenuItem onClick={() => setRoleFilter("admin")}>Admin</DropdownMenuItem>
<DropdownMenuItem onClick={() => setRoleFilter("user")}>User</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<Table>
<TableHeader><TableRow>
<TableHead>Name</TableHead><TableHead>Email</TableHead><TableHead>Role</TableHead><TableHead>Status</TableHead>
</TableRow></TableHeader>
<TableBody>{filtered.map((u) => (
<TableRow key={u.id}>
<TableCell className="font-medium">{u.name}</TableCell>
<TableCell>{u.email}</TableCell>
<TableCell><Badge variant={u.role === "admin" ? "default" : "secondary"}>{u.role}</Badge></TableCell>
<TableCell><Badge variant={u.status === "active" ? "default" : "outline"}>{u.status}</Badge></TableCell>
</TableRow>
))}</TableBody>
</Table>
</div>
);
}
```
---
## Dashboard Layout with Sidebar
`npx shadcn@latest add button separator sheet avatar`
```tsx
import { ReactNode } from "react";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
const navItems = [
{ label: "Dashboard", href: "/", active: true },
{ label: "Projects", href: "/projects" },
{ label: "Settings", href: "/settings" },
];
function SidebarNav() {
return (
<div className="flex h-full flex-col">
<div className="px-4 py-5 text-lg font-bold">App Name</div>
<Separator />
<nav className="flex-1 space-y-1 px-3 py-4">
{navItems.map((item) => (
<a key={item.href} href={item.href} className={`block rounded-md px-3 py-2 text-sm font-medium ${
item.active ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted"}`}>
{item.label}
</a>
))}
</nav>
<Separator />
<div className="flex items-center gap-3 px-4 py-4">
<Avatar className="h-8 w-8"><AvatarImage src="/avatar.jpg" /><AvatarFallback>JD</AvatarFallback></Avatar>
<div className="truncate">
<p className="text-sm font-medium">Jane Doe</p>
<p className="text-xs text-muted-foreground">jane@example.com</p>
</div>
</div>
</div>
);
}
export function DashboardLayout({ children }: { children: ReactNode }) {
return (
<div className="flex h-screen">
<aside className="hidden w-64 border-r bg-card lg:block"><SidebarNav /></aside>
<div className="flex flex-1 flex-col">
<header className="flex h-14 items-center gap-4 border-b px-4 lg:hidden">
<Sheet>
<SheetTrigger asChild><Button variant="ghost" size="icon">Menu</Button></SheetTrigger>
<SheetContent side="left" className="w-64 p-0"><SidebarNav /></SheetContent>
</Sheet>
</header>
<main className="flex-1 overflow-y-auto p-6 lg:p-8">{children}</main>
</div>
</div>
);
}
```
---
## Tips
- Use `cn()` from `@/lib/utils` to merge conditional classes
- Pair with `react-hook-form` + `zod` for form validation
- Use `Sheet` for mobile sidebars, `Dialog` for modals
- Customize theme in `globals.css` using CSS variables
@@ -1,231 +0,0 @@
# Tailwind CSS UI Pattern Recipes
Copy-paste patterns for common UI components. All examples use Tailwind v3+ utility classes.
---
## Responsive Navbar
```html
<nav class="bg-white shadow">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 items-center justify-between">
<div class="flex items-center gap-8">
<a href="/" class="text-xl font-bold text-gray-900">Logo</a>
<div class="hidden md:flex md:gap-6">
<a href="#" class="text-sm font-medium text-gray-700 hover:text-gray-900">Dashboard</a>
<a href="#" class="text-sm font-medium text-gray-500 hover:text-gray-900">Projects</a>
<a href="#" class="text-sm font-medium text-gray-500 hover:text-gray-900">Settings</a>
</div>
</div>
<div class="flex items-center gap-4">
<button class="rounded-full bg-gray-100 p-2 text-gray-600 hover:bg-gray-200">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6 6 0 10-12 0v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0a3 3 0 11-6 0m6 0H9" />
</svg>
</button>
<img class="h-8 w-8 rounded-full" src="https://via.placeholder.com/32" alt="Avatar" />
</div>
<!-- Mobile menu button -->
<button class="md:hidden rounded p-2 text-gray-600 hover:bg-gray-100">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
</nav>
```
---
## Card Grid
```html
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<!-- Card -->
<div class="overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm
transition hover:shadow-md">
<img class="h-48 w-full object-cover" src="https://via.placeholder.com/400x200" alt="" />
<div class="p-5">
<h3 class="text-lg font-semibold text-gray-900">Card Title</h3>
<p class="mt-2 text-sm text-gray-600">
Brief description of the card content goes here.
</p>
<div class="mt-4 flex items-center justify-between">
<span class="inline-flex items-center rounded-full bg-blue-50 px-2.5 py-0.5
text-xs font-medium text-blue-700">Category</span>
<a href="#" class="text-sm font-medium text-blue-600 hover:text-blue-500">
View details &rarr;
</a>
</div>
</div>
</div>
<!-- Repeat cards... -->
</div>
```
---
## Hero Section
```html
<section class="bg-white">
<div class="mx-auto max-w-7xl px-4 py-24 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl text-center">
<span class="inline-block rounded-full bg-blue-50 px-3 py-1 text-sm
font-medium text-blue-700">New Release</span>
<h1 class="mt-4 text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
Build faster with modern tools
</h1>
<p class="mt-6 text-lg leading-8 text-gray-600">
A concise value proposition that explains what the product does
and why users should care.
</p>
<div class="mt-10 flex items-center justify-center gap-4">
<a href="#" class="rounded-lg bg-blue-600 px-6 py-3 text-sm font-semibold
text-white shadow-sm hover:bg-blue-500">Get started</a>
<a href="#" class="text-sm font-semibold text-gray-900 hover:text-gray-700">
Learn more &rarr;
</a>
</div>
</div>
</div>
</section>
```
---
## Form Layout
```html
<form class="mx-auto max-w-lg space-y-6">
<div>
<label for="name" class="block text-sm font-medium text-gray-700">Full Name</label>
<input type="text" id="name" name="name"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2
text-sm shadow-sm placeholder:text-gray-400
focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" />
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
<input type="email" id="email" name="email"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2
text-sm shadow-sm placeholder:text-gray-400
focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" />
<p class="mt-1 text-sm text-gray-500">We'll never share your email.</p>
</div>
<div>
<label for="message" class="block text-sm font-medium text-gray-700">Message</label>
<textarea id="message" name="message" rows="4"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2
text-sm shadow-sm placeholder:text-gray-400
focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"></textarea>
</div>
<div class="flex items-center justify-end gap-3">
<button type="button" class="rounded-md px-4 py-2 text-sm font-medium
text-gray-700 hover:bg-gray-50">Cancel</button>
<button type="submit" class="rounded-md bg-blue-600 px-4 py-2 text-sm
font-semibold text-white shadow-sm
hover:bg-blue-500">Submit</button>
</div>
</form>
```
---
## Modal Overlay
```html
<!-- Backdrop -->
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<!-- Modal -->
<div class="w-full max-w-md rounded-xl bg-white p-6 shadow-xl">
<div class="flex items-start justify-between">
<h2 class="text-lg font-semibold text-gray-900">Confirm Action</h2>
<button class="rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<p class="mt-3 text-sm text-gray-600">
Are you sure you want to proceed? This action cannot be undone.
</p>
<div class="mt-6 flex justify-end gap-3">
<button class="rounded-md px-4 py-2 text-sm font-medium text-gray-700
hover:bg-gray-50">Cancel</button>
<button class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold
text-white hover:bg-red-500">Delete</button>
</div>
</div>
</div>
```
---
## Sidebar Layout
```html
<div class="flex h-screen">
<!-- Sidebar -->
<aside class="hidden w-64 flex-shrink-0 border-r border-gray-200 bg-gray-50 lg:block">
<div class="flex h-16 items-center px-6">
<span class="text-lg font-bold text-gray-900">App Name</span>
</div>
<nav class="space-y-1 px-3 py-4">
<a href="#" class="flex items-center gap-3 rounded-md bg-blue-50 px-3 py-2
text-sm font-medium text-blue-700">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0h4" />
</svg>
Dashboard
</a>
<a href="#" class="flex items-center gap-3 rounded-md px-3 py-2 text-sm
font-medium text-gray-700 hover:bg-gray-100">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" />
</svg>
Users
</a>
<a href="#" class="flex items-center gap-3 rounded-md px-3 py-2 text-sm
font-medium text-gray-700 hover:bg-gray-100">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Settings
</a>
</nav>
</aside>
<!-- Main content -->
<main class="flex-1 overflow-y-auto bg-white">
<div class="px-6 py-8 lg:px-8">
<h1 class="text-2xl font-bold text-gray-900">Dashboard</h1>
<div class="mt-6">
<!-- Page content here -->
</div>
</div>
</main>
</div>
```
---
## Tips
- Use `transition` and `hover:` for interactive feedback
- Use `focus-visible:` instead of `focus:` for keyboard-only focus rings
- Use `dark:` variants when supporting dark mode
- Prefer `gap-*` over margin utilities for flex/grid spacing
- Use `max-w-7xl mx-auto px-4 sm:px-6 lg:px-8` as a standard container
+62
View File
@@ -0,0 +1,62 @@
---
name: languages
description: >
Use when working with Python, TypeScript, or JavaScript language-specific patterns — including type hints, generics, async/await, dataclasses, Pydantic, PEP 8, strict mode, tsconfig.json, Zod schemas, ESM/CJS, destructuring, optional chaining, or language idioms.
---
# Languages
## When to Use
- Python files (.py) — type hints, async/await, dataclasses, Pydantic, context managers, PEP 8
- TypeScript files (.ts, .tsx) — strict mode, generics, utility types, Zod, discriminated unions
- JavaScript files (.js, .mjs, .cjs) — ES6+ patterns, ESM/CJS, ESLint, modern syntax
- Language-specific idioms, package management (pip, pnpm), or migration between languages
## When NOT to Use
- Framework-specific patterns — use `backend-frameworks` or `frontend`
- Testing — use `testing`
- Database queries — use `databases`
---
## Quick Reference
| Language | Reference | Key features |
|----------|-----------|-------------|
| Python | `references/python.md` | Type hints, dataclasses, Pydantic, asyncio, context managers, PEP 8 |
| TypeScript | `references/typescript.md` | Strict mode, generics, utility types, Zod, discriminated unions, satisfies |
| JavaScript | `references/javascript.md` | ES6+, async/await, ESM/CJS, destructuring, private fields, structuredClone |
---
## Best Practices
1. **Use type hints on all public functions** (Python) / **enable strict mode** (TypeScript).
2. **Prefer dataclasses or Pydantic for structured data** (Python) / **interfaces for object shapes** (TypeScript).
3. **Use context managers for resource management** (Python).
4. **Never use `any`** — use `unknown` instead (TypeScript).
5. **Use `const` by default, `let` when needed, never `var`** (JavaScript).
6. **Validate external data at boundaries** — Zod (TypeScript) or Pydantic (Python).
7. **Handle all promise rejections** (JavaScript/TypeScript).
8. **Follow PEP 8 / ESLint + Prettier** consistently.
## Common Pitfalls
1. **Mutable default arguments** (Python) — use `None` with default in function body.
2. **Blocking calls inside async functions** (Python) — use `asyncio`-compatible libraries.
3. **Overusing type assertions `as`** (TypeScript) — use type guards instead.
4. **Implicit type coercion** (JavaScript) — always use `===` and `!==`.
5. **Forgetting `await`** (all three languages).
6. **Circular imports** (Python) / **circular dependencies** (TypeScript).
7. **`this` binding in callbacks** (JavaScript) — use arrow functions.
8. **Using enums instead of const objects** (TypeScript).
---
## Related Skills
- `backend-frameworks` — Framework-specific patterns
- `testing` — Language-specific test frameworks
- `error-handling` — Exception handling patterns
@@ -1,247 +0,0 @@
# Modern JavaScript Patterns Quick Reference
> ES2020+ patterns. All examples work in current Node.js (18+) and modern browsers.
## Destructuring Tricks
```javascript
// Nested destructuring with rename and default
const { data: { users: members = [] } = {} } = response;
// Array destructuring: skip elements
const [first, , third] = [1, 2, 3];
// Swap variables
[a, b] = [b, a];
// Rest in both arrays and objects
const { id, ...rest } = user;
const [head, ...tail] = items;
// Destructure function parameters
function draw({ x = 0, y = 0, color = "black" } = {}) { /* ... */ }
// Dynamic property destructuring
const key = "name";
const { [key]: value } = { name: "Alice" }; // value = "Alice"
// Destructure from iterables
const [a, b] = new Map([["a", 1], ["b", 2]]);
```
## Optional Chaining (?.)
```javascript
// Property access
const city = user?.address?.city;
// Method call (only calls if method exists)
const result = api?.getData?.();
// Bracket notation
const val = obj?.["dynamic-key"];
// Array index
const first = arr?.[0];
// Combine with nullish coalescing
const name = user?.profile?.name ?? "Anonymous";
// Short-circuit: stops evaluating after first nullish
const len = response?.data?.items?.length; // undefined if any is nullish
```
## Nullish Coalescing (??) vs OR (||)
```javascript
// ?? only triggers on null/undefined (NOT 0, "", false)
0 ?? "fallback" // 0
"" ?? "fallback" // ""
null ?? "fallback" // "fallback"
// || triggers on any falsy value
0 || "fallback" // "fallback"
"" || "fallback" // "fallback"
null || "fallback" // "fallback"
// Use ?? for values where 0/empty string are valid
const port = config.port ?? 3000;
const title = config.title ?? "Untitled";
```
## Logical Assignment Operators
```javascript
// ??= assigns only if current value is null/undefined
user.name ??= "Anonymous";
// Equivalent: user.name = user.name ?? "Anonymous"
// ||= assigns if current value is falsy
opts.verbose ||= false;
// Equivalent: opts.verbose = opts.verbose || false
// &&= assigns only if current value is truthy
user.token &&= encrypt(user.token);
// Equivalent: user.token = user.token && encrypt(user.token)
```
## structuredClone (Deep Copy)
```javascript
// Deep clone objects, arrays, Maps, Sets, Dates, RegExp, etc.
const original = { date: new Date(), nested: { arr: [1, 2] } };
const clone = structuredClone(original);
clone.nested.arr.push(3); // original not affected
// Works with circular references
const obj = { self: null };
obj.self = obj;
const copy = structuredClone(obj); // OK
// Does NOT clone: functions, DOM nodes, symbols, prototype chain
// Throws on: functions, Error objects (in some engines)
```
## Proxy
```javascript
// Validation proxy
const validated = new Proxy({}, {
set(target, prop, value) {
if (prop === "age" && (typeof value !== "number" || value < 0)) {
throw new TypeError("Age must be a non-negative number");
}
target[prop] = value;
return true;
}
});
// Read-only proxy
function readonly(target) {
return new Proxy(target, {
set() { throw new Error("Read-only object"); },
deleteProperty() { throw new Error("Read-only object"); }
});
}
// Default values proxy
function withDefaults(target, defaults) {
return new Proxy(target, {
get(obj, prop) {
return prop in obj ? obj[prop] : defaults[prop];
}
});
}
const config = withDefaults({}, { theme: "dark", lang: "en" });
config.theme; // "dark"
// Logging / observation proxy
function observable(target, onChange) {
return new Proxy(target, {
set(obj, prop, value) {
const old = obj[prop];
obj[prop] = value;
onChange(prop, old, value);
return true;
}
});
}
```
## Generators
```javascript
// Basic generator
function* range(start, end, step = 1) {
for (let i = start; i < end; i += step) {
yield i;
}
}
for (const n of range(0, 10, 2)) { /* 0, 2, 4, 6, 8 */ }
// Infinite sequence
function* ids() {
let id = 0;
while (true) yield id++;
}
const gen = ids();
gen.next().value; // 0
gen.next().value; // 1
// Delegate to another generator
function* concat(...iterables) {
for (const it of iterables) {
yield* it;
}
}
// Two-way communication
function* stateMachine() {
let input;
while (true) {
input = yield `received: ${input}`;
}
}
const sm = stateMachine();
sm.next(); // { value: "received: undefined" }
sm.next("hello"); // { value: "received: hello" }
```
## Async Iterators
```javascript
// for-await-of
async function processStream(stream) {
for await (const chunk of stream) {
console.log(chunk);
}
}
// Async generator
async function* fetchPages(url) {
let page = 1;
while (true) {
const res = await fetch(`${url}?page=${page}`);
const data = await res.json();
if (data.items.length === 0) return;
yield data.items;
page++;
}
}
for await (const items of fetchPages("/api/users")) {
console.log(items);
}
```
## Other Modern Patterns
```javascript
// Object.groupBy (ES2024)
const grouped = Object.groupBy(users, u => u.role);
// at() - negative indexing
[1, 2, 3].at(-1); // 3
// Object.hasOwn (replaces hasOwnProperty)
Object.hasOwn(obj, "key"); // true/false
// Error cause chaining
throw new Error("DB failed", { cause: originalError });
// AbortSignal.timeout (built-in timeout)
fetch(url, { signal: AbortSignal.timeout(5000) });
// using keyword (explicit resource management, ES2024+)
{ using handle = openFile("data.txt"); } // auto-disposed at block exit
```
## Promise Combinators
| Method | Settles when | Returns |
|--------|-------------|---------|
| `Promise.all(ps)` | All fulfill or one rejects | Array of values |
| `Promise.allSettled(ps)` | All settle | Array of `{status, value/reason}` |
| `Promise.race(ps)` | First settles | First value or rejection |
| `Promise.any(ps)` | First fulfills | First value (AggregateError if all reject) |
@@ -1,216 +0,0 @@
# Python Type Hints Quick Reference
> Python 3.10+ syntax preferred. For 3.9, use `from __future__ import annotations`.
## Basic Types
| Type | Example | Notes |
|------|---------|-------|
| `int` | `x: int = 1` | |
| `float` | `x: float = 1.0` | |
| `str` | `x: str = "hi"` | |
| `bool` | `x: bool = True` | |
| `bytes` | `x: bytes = b"hi"` | |
| `None` | `x: None = None` | Use as return type for side-effect functions |
| `object` | `x: object` | Accepts anything, but no attribute access |
| `Any` | `x: Any` | Escapes type checking entirely |
## Collection Types (3.10+)
| Type | Example | Notes |
|------|---------|-------|
| `list[int]` | `x: list[int] = [1, 2]` | Mutable sequence |
| `tuple[int, str]` | `x: tuple[int, str]` | Fixed length |
| `tuple[int, ...]` | `x: tuple[int, ...]` | Variable length |
| `dict[str, int]` | `x: dict[str, int]` | |
| `set[str]` | `x: set[str]` | |
| `frozenset[str]` | `x: frozenset[str]` | |
## Union and Optional
```python
# 3.10+ syntax
def f(x: int | str) -> None: ...
def g(x: int | None = None) -> None: ...
# Pre-3.10
from typing import Union, Optional
def f(x: Union[int, str]) -> None: ...
def g(x: Optional[int] = None) -> None: ...
```
## TypeAlias
```python
from typing import TypeAlias
# Explicit alias (3.10+)
Vector: TypeAlias = list[float]
# 3.12+ syntax
type Vector = list[float]
type Tree[T] = T | list["Tree[T]"] # recursive
```
## Generics with TypeVar
```python
from typing import TypeVar
T = TypeVar("T")
K = TypeVar("K", bound=str) # upper bound
N = TypeVar("N", int, float) # constrained
def first(items: list[T]) -> T:
return items[0]
# 3.12+ syntax (no TypeVar needed)
def first[T](items: list[T]) -> T:
return items[0]
```
## ParamSpec and Concatenate
```python
from typing import ParamSpec, Concatenate, Callable
P = ParamSpec("P")
T = TypeVar("T")
# Preserve function signatures through decorators
def logged(fn: Callable[P, T]) -> Callable[P, T]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
print(f"Calling {fn.__name__}")
return fn(*args, **kwargs)
return wrapper
# Add a parameter to a function signature
def with_user(
fn: Callable[Concatenate[User, P], T]
) -> Callable[P, T]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
return fn(get_current_user(), *args, **kwargs)
return wrapper
```
## Protocol (Structural Typing)
```python
from typing import Protocol, runtime_checkable
class Renderable(Protocol):
def render(self) -> str: ...
class Widget(Protocol):
name: str
def resize(self, width: int, height: int) -> None: ...
# Any class with matching methods satisfies the protocol
class Button:
def render(self) -> str:
return "<button/>"
def draw(item: Renderable) -> None: # Button works here
print(item.render())
# Runtime checking
@runtime_checkable
class Sized(Protocol):
def __len__(self) -> int: ...
assert isinstance([1, 2], Sized) # True at runtime
```
## @overload
```python
from typing import overload
@overload
def parse(data: str) -> dict[str, Any]: ...
@overload
def parse(data: bytes) -> list[int]: ...
@overload
def parse(data: str, raw: Literal[True]) -> str: ...
def parse(data: str | bytes, raw: bool = False) -> dict | list | str:
"""Implementation handles all overloads."""
...
```
## TypeGuard and TypeIs
```python
from typing import TypeGuard, TypeIs # TypeIs: 3.13+
# TypeGuard: narrows type in True branch only
def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in val)
# TypeIs: narrows in both True and False branches
def is_int(val: int | str) -> TypeIs[int]:
return isinstance(val, int)
def f(val: int | str) -> None:
if is_int(val):
reveal_type(val) # int
else:
reveal_type(val) # str (only with TypeIs)
```
## Literal and Final
```python
from typing import Literal, Final
def set_mode(mode: Literal["read", "write", "append"]) -> None: ...
MAX_SIZE: Final = 100 # Cannot be reassigned
PREFIX: Final[str] = "app_" # With explicit type
```
## Callable
| Signature | Meaning |
|-----------|---------|
| `Callable[[int, str], bool]` | Function taking int and str, returning bool |
| `Callable[..., bool]` | Any args, returning bool |
| `Callable[P, T]` | Generic (use with ParamSpec) |
## TypedDict
```python
from typing import TypedDict, NotRequired, Required
class Config(TypedDict):
name: str
debug: NotRequired[bool] # optional key
class PartialConfig(TypedDict, total=False):
name: Required[str] # required even though total=False
debug: bool # optional
```
## Common Patterns
| Pattern | Type Hint |
|---------|-----------|
| JSON value | `dict[str, Any]` or custom TypedDict |
| Decorator preserving sig | `Callable[P, T] -> Callable[P, T]` |
| Context manager | `AbstractContextManager[T]` |
| Async context manager | `AbstractAsyncContextManager[T]` |
| Generator | `Generator[YieldType, SendType, ReturnType]` |
| Async generator | `AsyncGenerator[YieldType, SendType]` |
| Class method returning self | `-> Self` (3.11+, `from typing import Self`) |
| Numeric tower | `int | float` (avoid `numbers.Number`) |
## Type Narrowing Cheat Sheet
| Technique | Example |
|-----------|---------|
| `isinstance` | `if isinstance(x, str):` |
| `is None` / `is not None` | `if x is not None:` |
| `TypeGuard` / `TypeIs` | Custom narrowing functions |
| `assert` | `assert isinstance(x, str)` |
| `Literal` checks | `if x == "read":` |
| `hasattr` | `if hasattr(x, "render"):` (limited) |
@@ -1,8 +1,5 @@
---
name: javascript
description: >
Trigger this skill whenever working with JavaScript files (.js, .mjs, .cjs), writing Node.js applications without TypeScript, or using ES6+ patterns like destructuring, async/await, optional chaining, and modules. Activate for browser scripting, vanilla JS projects, or when the user asks about JavaScript-specific idioms, ESLint configuration, or modern syntax. Also use when dealing with package.json scripts, CommonJS vs ESM, or JavaScript class patterns.
---
# Languages — JavaScript Patterns
# JavaScript
@@ -14,7 +11,7 @@ description: >
## When NOT to Use
- TypeScript projects -- use the `languages/typescript` skill instead, which covers typed JavaScript patterns
- TypeScript projects -- use the `typescript` skill instead, which covers typed JavaScript patterns
- Python-only projects with no JavaScript components
---
@@ -718,7 +715,7 @@ performance.clearMeasures();
## Related Skills
- `languages/typescript` -- TypeScript for typed JavaScript development
- `frameworks/react` -- React component patterns
- `frameworks/nextjs` -- Next.js full-stack framework
- `testing/vitest` -- JavaScript/TypeScript testing with Vitest
- `typescript` -- TypeScript for typed JavaScript development
- `react` -- React component patterns
- `nextjs` -- Next.js full-stack framework
- `vitest` -- JavaScript/TypeScript testing with Vitest
@@ -1,8 +1,5 @@
---
name: python
description: >
Trigger this skill whenever working with Python files (.py), writing Python scripts or applications, or using Python frameworks like Django, FastAPI, or Flask. Activate for any Python-specific patterns including type hints, async/await with asyncio, dataclasses, Pydantic models, context managers, virtual environments, or PEP 8 style questions. Also use when the user references Python package management, pip, or pyproject.toml.
---
# Languages — Python Patterns
# Python
@@ -693,8 +690,8 @@ with suppress(FileNotFoundError):
## Related Skills
- `languages/typescript` -- TypeScript language patterns for polyglot projects
- `frameworks/fastapi` -- FastAPI web framework built on Python
- `frameworks/django` -- Django web framework for Python
- `testing/pytest` -- Python testing with pytest
- `patterns/error-handling` -- Python error handling and exception hierarchies
- `typescript` -- TypeScript language patterns for polyglot projects
- `fastapi` -- FastAPI web framework built on Python
- `django` -- Django web framework for Python
- `pytest` -- Python testing with pytest
- `error-handling` -- Python error handling and exception hierarchies
@@ -1,8 +1,5 @@
---
name: typescript
description: >
Trigger this skill whenever working with TypeScript files (.ts, .tsx), configuring tsconfig.json, or using TypeScript-specific features like strict typing, generics, utility types, or type guards. Activate for any TypeScript project setup, type definition authoring, Zod schema validation, or discriminated union patterns. Also use when the user asks about avoiding `any`, enabling strict mode, or migrating JavaScript to TypeScript.
---
# Languages — TypeScript Patterns
# TypeScript
@@ -684,10 +681,10 @@ function getLabel(role: "admin" | "user" | "guest"): string {
## Related Skills
- `languages/javascript` -- JavaScript patterns for JS interop and migration
- `languages/python` -- Python language patterns for polyglot projects
- `frameworks/react` -- React component patterns with TypeScript
- `frameworks/nextjs` -- Next.js framework with TypeScript support
- `testing/vitest` -- TypeScript testing with Vitest
- `patterns/error-handling` -- TypeScript error handling patterns
- `patterns/state-management` -- State management with TypeScript types
- `javascript` -- JavaScript patterns for JS interop and migration
- `python` -- Python language patterns for polyglot projects
- `react` -- React component patterns with TypeScript
- `nextjs` -- Next.js framework with TypeScript support
- `vitest` -- TypeScript testing with Vitest
- `error-handling` -- TypeScript error handling patterns
- `state-management` -- State management with TypeScript types
@@ -1,249 +0,0 @@
# TypeScript Advanced Types Quick Reference
## Discriminated Unions
```typescript
// Tag each variant with a literal type field
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.radius ** 2;
case "rect": return s.width * s.height;
case "triangle": return (s.base * s.height) / 2;
}
}
// Exhaustiveness check helper
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}
```
## Branded Types
```typescript
// Prevent mixing structurally identical types
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
function getUser(id: UserId) { /* ... */ }
const userId = "abc" as UserId;
const orderId = "abc" as OrderId;
getUser(userId); // OK
getUser(orderId); // Error: OrderId not assignable to UserId
// Validation-based branding
type Email = Brand<string, "Email">;
function parseEmail(input: string): Email {
if (!input.includes("@")) throw new Error("Invalid email");
return input as Email;
}
```
## Template Literal Types
```typescript
// Build string types from unions
type Method = "get" | "post" | "put" | "delete";
type Route = "/users" | "/orders";
type Endpoint = `${Uppercase<Method>} ${Route}`;
// "GET /users" | "GET /orders" | "POST /users" | ...
// Event handler pattern
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
// Extract parts from string types
type ExtractParam<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParam<Rest>
: T extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractParam<"/users/:id/posts/:postId">;
// "id" | "postId"
```
## Conditional Types
```typescript
// Basic conditional
type IsString<T> = T extends string ? true : false;
// Distributive over unions (when T is naked type parameter)
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>; // string[] | number[]
// Prevent distribution with tuple wrapping
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>; // (string | number)[]
// infer keyword
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
type Unpacked<T> = T extends Promise<infer U> ? U :
T extends Array<infer U> ? U : T;
// infer with constraints (TS 4.7+)
type FirstString<T> =
T extends [infer S extends string, ...unknown[]] ? S : never;
```
## Mapped Types
```typescript
// Transform all properties
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Optional<T> = { [K in keyof T]?: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
// Map to new value types
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person { name: string; age: number }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
```
## Key Remapping (via `as`)
```typescript
// Filter keys
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
// Rename keys
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};
// Remove specific keys
type RemoveKind<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K];
};
// Build from union
type EventMap<T extends string> = {
[K in T as `on${Capitalize<K>}`]: (event: K) => void;
};
type Handlers = EventMap<"click" | "scroll">;
// { onClick: (event: "click") => void; onScroll: (event: "scroll") => void }
```
## Recursive Types
```typescript
// JSON type
type Json =
| string
| number
| boolean
| null
| Json[]
| { [key: string]: Json };
// Deep readonly
type DeepReadonly<T> = T extends Function
? T
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
// Deep partial
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
// Flatten nested object paths
type Paths<T, Prefix extends string = ""> = T extends object
? {
[K in keyof T & string]: T[K] extends object
? Paths<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`;
}[keyof T & string]
: never;
type P = Paths<{ a: { b: number; c: { d: string } } }>;
// "a.b" | "a.c.d"
```
## Utility Types Cheat Sheet
| Utility | Effect |
|---------|--------|
| `Partial<T>` | All properties optional |
| `Required<T>` | All properties required |
| `Readonly<T>` | All properties readonly |
| `Record<K, V>` | Object with keys K and values V |
| `Pick<T, K>` | Subset of properties |
| `Omit<T, K>` | All except listed properties |
| `Exclude<U, E>` | Remove members from union |
| `Extract<U, E>` | Keep matching members from union |
| `NonNullable<T>` | Remove null and undefined |
| `ReturnType<F>` | Return type of function |
| `Parameters<F>` | Tuple of parameter types |
| `ConstructorParameters<C>` | Constructor parameter types |
| `InstanceType<C>` | Instance type of constructor |
| `Awaited<T>` | Unwrap Promise (deeply) |
| `NoInfer<T>` | Prevent inference from this position (5.4+) |
## Satisfies Operator (4.9+)
```typescript
// Validate type without widening
const palette = {
red: "#ff0000",
green: [0, 255, 0],
} satisfies Record<string, string | number[]>;
palette.red.toUpperCase(); // OK - knows it's string
palette.green.map(x => x); // OK - knows it's number[]
```
## const Type Parameters (5.0+)
```typescript
// Infer narrow literal types from arguments
function routes<const T extends readonly string[]>(paths: T): T {
return paths;
}
const r = routes(["/home", "/about"]);
// Type: readonly ["/home", "/about"] (not string[])
```
## Pattern: Type-Safe Event Emitter
```typescript
type EventMap = {
login: { userId: string };
logout: undefined;
error: { code: number; message: string };
};
class Emitter<E extends Record<string, unknown>> {
on<K extends keyof E>(
event: K,
handler: E[K] extends undefined
? () => void
: (payload: E[K]) => void
): void { /* ... */ }
emit<K extends keyof E>(
...args: E[K] extends undefined ? [K] : [K, E[K]]
): void { /* ... */ }
}
```
+104
View File
@@ -0,0 +1,104 @@
---
name: logging
description: >
Use when setting up loggers, choosing log levels, implementing correlation IDs for request tracing, redacting sensitive data from logs, or configuring log aggregation. Also activate whenever code uses console.log, print(), logging module, winston, pino, structlog, or any logging library. Applies when building observability, debugging production issues, or adding telemetry.
---
# Logging
## When to Use
- Setting up structured logging in a new application or service
- Replacing `console.log` or `print()` with proper logging infrastructure
- Adding request tracing with correlation IDs across microservices
- Redacting sensitive data (passwords, tokens, PII) from log output
- Building observability pipelines with log aggregation (ELK, Datadog, CloudWatch)
## When NOT to Use
- Static analysis or linting tasks that do not involve runtime output
- Pure computation functions where logging would add unnecessary noise
- Test assertions — use testing frameworks' built-in assertion messages, not log output
---
## Quick Reference
| # | Pattern | Description |
|---|---------|-------------|
| 1 | Structured Logging Setup | Configure JSON-based structured logging at application startup with environment-aware renderers |
| 2 | Log Levels | Use DEBUG/INFO/WARNING/ERROR/CRITICAL consistently to control verbosity and enable filtering |
| 3 | Correlation IDs | Generate a unique request ID at the entry point and propagate it through all downstream calls |
| 4 | Sensitive Data Redaction | Build redaction into the logging pipeline so secrets and PII are never written to logs |
| 5 | Request/Response Logging | Log every HTTP request/response with method, path, status, duration, and body size |
| 6 | Error Logging | Include stack traces, relevant IDs, and enough context to reproduce without production access |
| 7 | Performance Logging | Track operation durations to identify slow endpoints, queries, and external calls |
---
## Log Levels
| Level | When to Use | Example |
|-------|-------------|---------|
| `DEBUG` | Detailed diagnostic information useful only during development or debugging | Variable values, SQL queries, cache hits/misses |
| `INFO` | Confirmation that things are working as expected | Request received, user created, job completed |
| `WARNING` | Something unexpected happened but the application can continue | Deprecated API called, retry attempt, approaching rate limit |
| `ERROR` | A specific operation failed but the application continues running | Database query failed, external API returned 500, payment declined |
| `CRITICAL` | The application cannot continue or is in an unrecoverable state | Database connection pool exhausted, out of disk space, configuration missing |
**Level selection rule of thumb:** If you would page someone at 3 AM, it is ERROR or CRITICAL. If it is useful context for investigating an issue, it is INFO. If it is only useful when actively debugging a specific problem, it is DEBUG.
---
## Language References
See `references/python-patterns.md` for Python/structlog examples.
See `references/typescript-patterns.md` for TypeScript/pino examples.
---
## Best Practices
1. **Use structured logging from day one** — start with JSON output and key-value pairs instead of formatted strings. Switching from `f"User {user_id} created"` to `logger.info("user_created", user_id=user_id)` costs nothing upfront but saves hours when debugging in production.
2. **Log events, not sentences** — use snake_case event names (`order_placed`, `payment_failed`) rather than prose messages (`"An order was placed by the user"`). Event names are searchable, filterable, and easy to aggregate.
3. **Include the right context at the right level** — every log line should include enough context to be useful in isolation: relevant IDs (user, order, request), operation name, and outcome. Avoid logging the same error at every layer of the call stack.
4. **Set log levels per environment** — use DEBUG in development, INFO in staging, and INFO or WARNING in production. Never leave DEBUG enabled in production — it generates excessive volume and may expose sensitive internals.
5. **Centralize logging configuration** — configure loggers once at application startup, not in individual modules. Every module should call `get_logger(__name__)` and inherit the shared configuration.
6. **Always redact sensitive data** — build redaction into the logging pipeline as a processor or serializer. Do not rely on developers remembering to exclude passwords or tokens from log calls.
7. **Use correlation IDs for every request** — generate a unique ID at the entry point and propagate it through all downstream calls. This is the single most important pattern for debugging distributed systems.
8. **Set up log rotation and retention policies** — configure maximum file sizes, rotation intervals, and retention periods. Production logs without rotation will fill disks. Use log aggregation services (ELK, Datadog, CloudWatch) rather than relying on local files.
---
## Common Pitfalls
1. **Logging sensitive data** — passwords, API keys, JWTs, credit card numbers, and PII end up in logs more often than expected. Once written, they persist in log storage and backups. Build redaction into the pipeline rather than relying on code review to catch every instance.
2. **Using print() or console.log in production**`print()` in Python and `console.log` in Node.js write to stdout without timestamps, levels, or structure. They cannot be filtered, aggregated, or searched. Replace them with a proper logger before deploying.
3. **Logging too much at high levels** — setting every log call to INFO or ERROR creates alert fatigue and obscures real problems. Use DEBUG for diagnostic details and reserve ERROR for situations that require action.
4. **Missing stack traces on errors** — logging `str(exception)` loses the stack trace. In Python, use `exc_info=True` or `logger.exception()`. In pino, pass the error as `{ err }` to get the full stack serialized.
5. **Not testing log output** — logging code is code. If your redaction processor has a bug, secrets leak. Write unit tests that capture log output and assert on structure, redacted fields, and expected context.
6. **Synchronous logging in async applications** — writing to files or network sinks synchronously from an async event loop blocks request processing. Use async-compatible transports (pino's worker thread, structlog with stdlib async handlers) or write to stdout and let the infrastructure handle routing.
---
## Related Skills
- `error-handling` — Exception handling patterns that complement error logging
- `api-client` — HTTP client patterns including logging outbound requests
- `fastapi` — FastAPI middleware setup for request logging and correlation IDs
- `docker` — Container logging drivers and log aggregation in Docker environments
- `postgresql` — Logging database queries and slow query detection
- `mongodb` — Logging database operations and aggregation pipelines
@@ -0,0 +1,506 @@
# Logging — Python Patterns (structlog)
Reference examples for the [logging skill](./SKILL.md). All patterns use [structlog](https://www.structlog.org/) with stdlib integration.
---
## 1. Structured Logging Setup
Configure structured logging once at application startup. All modules then use `structlog.get_logger(__name__)`.
```python
# logging_config.py
import logging
import structlog
def configure_logging(log_level: str = "INFO", json_output: bool = True) -> None:
"""Configure structured logging for the application.
Call this once at application startup, before any loggers are created.
"""
# Set the stdlib logging level as the baseline filter
logging.basicConfig(
format="%(message)s",
level=getattr(logging, log_level.upper()),
)
# Choose renderers based on environment
if json_output:
renderer = structlog.processors.JSONRenderer()
else:
# Human-readable output for local development
renderer = structlog.dev.ConsoleRenderer(colors=True)
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
renderer,
],
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
```
```python
# Usage anywhere in the application
import structlog
logger = structlog.get_logger(__name__)
async def create_user(email: str) -> User:
logger.info("creating_user", email=email)
user = await user_repo.create(email=email)
logger.info("user_created", user_id=user.id, email=email)
return user
```
**Output (JSON mode):**
```json
{"event": "user_created", "user_id": 42, "email": "alice@example.com", "logger": "app.services.user", "level": "info", "timestamp": "2025-06-15T10:30:00.123Z"}
```
---
## 2. Log Levels
```python
import structlog
logger = structlog.get_logger(__name__)
# DEBUG: Detailed internals for troubleshooting
logger.debug("cache_lookup", key="user:42", hit=True, ttl_remaining=120)
# INFO: Normal business events
logger.info("order_placed", order_id="ORD-123", total=99.99, items=3)
# WARNING: Degraded but functional
logger.warning(
"rate_limit_approaching",
current_rate=450,
limit=500,
window_seconds=60,
)
# ERROR: Operation failed, needs attention
logger.error(
"payment_failed",
order_id="ORD-123",
provider="stripe",
error_code="card_declined",
exc_info=True, # Include stack trace
)
# CRITICAL: System-level failure
logger.critical(
"database_pool_exhausted",
active_connections=100,
max_connections=100,
waiting_requests=47,
)
```
---
## 3. Correlation IDs
Correlation IDs tie together all log entries from a single request. Uses FastAPI middleware with `contextvars`.
```python
# middleware/correlation.py
import uuid
from contextvars import ContextVar
import structlog
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
# Context variable accessible from any async task in the same request
correlation_id_var: ContextVar[str] = ContextVar("correlation_id", default="")
class CorrelationIDMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Accept an incoming correlation ID or generate a new one
correlation_id = request.headers.get("X-Correlation-ID", uuid.uuid4().hex)
correlation_id_var.set(correlation_id)
# Bind to structlog context so all logs in this request include it
structlog.contextvars.clear_contextvars()
structlog.contextvars.bind_contextvars(correlation_id=correlation_id)
response = await call_next(request)
response.headers["X-Correlation-ID"] = correlation_id
return response
```
```python
# Register the middleware
from middleware.correlation import CorrelationIDMiddleware
app.add_middleware(CorrelationIDMiddleware)
```
```python
# Any logger call in any module now includes correlation_id automatically
logger = structlog.get_logger(__name__)
async def get_user(user_id: int) -> User:
logger.info("fetching_user", user_id=user_id)
# Output: {"event": "fetching_user", "user_id": 42, "correlation_id": "a1b2c3d4...", ...}
return await user_repo.get(user_id)
```
### Propagating to downstream services
When calling other microservices, forward the correlation ID:
```python
# Python — httpx client
import httpx
from middleware.correlation import correlation_id_var
async def call_billing_service(user_id: int) -> dict:
correlation_id = correlation_id_var.get()
async with httpx.AsyncClient() as client:
response = await client.get(
f"http://billing-service/api/v1/invoices?user_id={user_id}",
headers={"X-Correlation-ID": correlation_id},
)
return response.json()
```
---
## 4. Sensitive Data Redaction
Build redaction into the logging pipeline as a structlog processor so developers cannot accidentally leak secrets.
```python
# processors/redact.py
import re
from typing import Any
# Patterns for sensitive field names (case-insensitive matching)
SENSITIVE_KEYS = re.compile(
r"(password|passwd|secret|token|api_key|apikey|authorization|"
r"credit_card|card_number|cvv|ssn|social_security)",
re.IGNORECASE,
)
# Pattern for credit card numbers in string values
CREDIT_CARD_PATTERN = re.compile(r"\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b")
# Pattern for bearer tokens in string values
BEARER_PATTERN = re.compile(r"Bearer\s+[A-Za-z0-9\-._~+/]+=*", re.IGNORECASE)
def redact_sensitive_data(
logger: Any, method_name: str, event_dict: dict
) -> dict:
"""Structlog processor that masks sensitive values."""
return _redact_dict(event_dict)
def _redact_dict(data: dict) -> dict:
result = {}
for key, value in data.items():
if SENSITIVE_KEYS.search(key):
result[key] = "***REDACTED***"
elif isinstance(value, dict):
result[key] = _redact_dict(value)
elif isinstance(value, str):
result[key] = _redact_string(value)
elif isinstance(value, list):
result[key] = [
_redact_dict(item) if isinstance(item, dict) else item
for item in value
]
else:
result[key] = value
return result
def _redact_string(value: str) -> str:
value = CREDIT_CARD_PATTERN.sub("****-****-****-****", value)
value = BEARER_PATTERN.sub("Bearer ***REDACTED***", value)
return value
```
```python
# Add the processor to structlog configuration
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
redact_sensitive_data, # Add before the renderer
structlog.processors.JSONRenderer(),
],
# ...
)
```
---
## 5. Request/Response Logging
Log every HTTP request and response with method, path, status code, duration, and body size. Uses FastAPI middleware.
```python
# middleware/request_logging.py
import time
import structlog
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
logger = structlog.get_logger("http")
class RequestLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start_time = time.perf_counter()
# Log request
logger.info(
"http_request_started",
method=request.method,
path=request.url.path,
query=str(request.url.query) or None,
client_ip=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent"),
)
try:
response = await call_next(request)
except Exception:
duration_ms = (time.perf_counter() - start_time) * 1000
logger.error(
"http_request_failed",
method=request.method,
path=request.url.path,
duration_ms=round(duration_ms, 2),
exc_info=True,
)
raise
duration_ms = (time.perf_counter() - start_time) * 1000
content_length = response.headers.get("content-length")
# Choose log level based on status code
log_method = logger.info
if response.status_code >= 500:
log_method = logger.error
elif response.status_code >= 400:
log_method = logger.warning
log_method(
"http_request_completed",
method=request.method,
path=request.url.path,
status_code=response.status_code,
duration_ms=round(duration_ms, 2),
content_length=int(content_length) if content_length else None,
)
return response
```
---
## 6. Error Logging
When logging errors, include the stack trace, relevant context, and enough information to reproduce the issue.
```python
import structlog
logger = structlog.get_logger(__name__)
async def process_order(order_id: str) -> Order:
logger.info("processing_order", order_id=order_id)
try:
order = await order_repo.get(order_id)
if not order:
logger.error("order_not_found", order_id=order_id)
raise OrderNotFoundError(order_id)
payment = await payment_service.charge(
amount=order.total,
currency=order.currency,
customer_id=order.customer_id,
)
logger.info(
"payment_processed",
order_id=order_id,
payment_id=payment.id,
amount=order.total,
)
except PaymentError as exc:
# Log the error with full context for debugging
logger.error(
"payment_failed",
order_id=order_id,
customer_id=order.customer_id,
amount=order.total,
error_code=exc.code,
error_message=str(exc),
exc_info=True, # Includes full stack trace
)
raise
except Exception as exc:
# Catch-all for unexpected errors
logger.exception(
"order_processing_unexpected_error",
order_id=order_id,
error_type=type(exc).__name__,
)
raise
```
---
## 7. Performance Logging
### Timing decorator
```python
import functools
import time
from typing import Callable, TypeVar
import structlog
logger = structlog.get_logger("performance")
F = TypeVar("F", bound=Callable)
def log_duration(operation: str, slow_threshold_ms: float = 1000.0):
"""Decorator that logs the duration of a function call.
Args:
operation: A descriptive name for the operation.
slow_threshold_ms: Threshold in milliseconds above which
the log level escalates to WARNING.
"""
def decorator(func: F) -> F:
@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
start = time.perf_counter()
try:
result = await func(*args, **kwargs)
return result
finally:
duration_ms = (time.perf_counter() - start) * 1000
log_fn = (
logger.warning
if duration_ms > slow_threshold_ms
else logger.debug
)
log_fn(
"operation_duration",
operation=operation,
duration_ms=round(duration_ms, 2),
slow=duration_ms > slow_threshold_ms,
)
@functools.wraps(func)
def sync_wrapper(*args, **kwargs):
start = time.perf_counter()
try:
result = func(*args, **kwargs)
return result
finally:
duration_ms = (time.perf_counter() - start) * 1000
log_fn = (
logger.warning
if duration_ms > slow_threshold_ms
else logger.debug
)
log_fn(
"operation_duration",
operation=operation,
duration_ms=round(duration_ms, 2),
slow=duration_ms > slow_threshold_ms,
)
import asyncio
if asyncio.iscoroutinefunction(func):
return async_wrapper # type: ignore
return sync_wrapper # type: ignore
return decorator
# Usage
@log_duration("fetch_user_profile", slow_threshold_ms=200)
async def get_user_profile(user_id: int) -> UserProfile:
return await user_repo.get_with_preferences(user_id)
```
### Context manager for ad-hoc timing
```python
import time
from contextlib import contextmanager
import structlog
logger = structlog.get_logger("performance")
@contextmanager
def log_timing(operation: str, **extra_fields):
"""Context manager for timing arbitrary code blocks."""
start = time.perf_counter()
yield
duration_ms = (time.perf_counter() - start) * 1000
logger.info(
"operation_duration",
operation=operation,
duration_ms=round(duration_ms, 2),
**extra_fields,
)
# Usage
async def rebuild_search_index():
with log_timing("rebuild_search_index", index="products"):
products = await product_repo.get_all()
await search_service.reindex(products)
```
### Slow query logging (SQLAlchemy)
```python
# SQLAlchemy event listener for slow queries
from sqlalchemy import event
from sqlalchemy.engine import Engine
import structlog
logger = structlog.get_logger("database")
SLOW_QUERY_THRESHOLD_MS = 500
@event.listens_for(Engine, "before_cursor_execute")
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
conn.info.setdefault("query_start_time", []).append(time.perf_counter())
@event.listens_for(Engine, "after_cursor_execute")
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
total_ms = (time.perf_counter() - conn.info["query_start_time"].pop()) * 1000
if total_ms > SLOW_QUERY_THRESHOLD_MS:
logger.warning(
"slow_query",
duration_ms=round(total_ms, 2),
statement=statement[:500], # Truncate long queries
parameters=str(parameters)[:200],
)
```
@@ -0,0 +1,404 @@
# Logging — TypeScript Patterns (pino)
Reference examples for the [logging skill](./SKILL.md). All patterns use [pino](https://github.com/pinojs/pino).
---
## 1. Structured Logging Setup
Configure pino once and export a factory for child loggers per module.
```typescript
// logger.ts
import pino from "pino";
export const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
// Use pretty printing only in development
transport:
process.env.NODE_ENV === "development"
? { target: "pino-pretty", options: { colorize: true } }
: undefined,
// Base fields included in every log line
base: {
service: process.env.SERVICE_NAME ?? "api",
version: process.env.APP_VERSION ?? "unknown",
},
// Customize serialization
serializers: {
err: pino.stdSerializers.err,
req: pino.stdSerializers.req,
res: pino.stdSerializers.res,
},
// Redact sensitive fields (see Pattern 4)
redact: ["req.headers.authorization", "req.headers.cookie"],
});
// Create child loggers for specific modules
export function createLogger(module: string): pino.Logger {
return logger.child({ module });
}
```
```typescript
// Usage in a service
import { createLogger } from "./logger";
const log = createLogger("user-service");
export async function createUser(email: string): Promise<User> {
log.info({ email }, "creating_user");
const user = await userRepo.create({ email });
log.info({ userId: user.id, email }, "user_created");
return user;
}
```
**Output (JSON):**
```json
{"level":30,"time":1718444400123,"service":"api","module":"user-service","userId":42,"email":"alice@example.com","msg":"user_created"}
```
---
## 2. Log Levels
```typescript
import { createLogger } from "./logger";
const log = createLogger("order-service");
// DEBUG: Internal details
log.debug({ key: "user:42", hit: true, ttlRemaining: 120 }, "cache_lookup");
// INFO: Normal events
log.info({ orderId: "ORD-123", total: 99.99, items: 3 }, "order_placed");
// WARNING: Degraded state
log.warn(
{ currentRate: 450, limit: 500, windowSeconds: 60 },
"rate_limit_approaching"
);
// ERROR: Operation failure
log.error(
{ orderId: "ORD-123", provider: "stripe", errorCode: "card_declined" },
"payment_failed"
);
// FATAL: Unrecoverable
log.fatal(
{ activeConnections: 100, maxConnections: 100, waitingRequests: 47 },
"database_pool_exhausted"
);
```
---
## 3. Correlation IDs
Correlation IDs tie together all log entries from a single request. Uses Express middleware with `AsyncLocalStorage`.
```typescript
// middleware/correlation.ts
import { AsyncLocalStorage } from "node:async_hooks";
import { randomUUID } from "node:crypto";
import type { Request, Response, NextFunction } from "express";
import { logger } from "../logger";
interface RequestContext {
correlationId: string;
}
export const asyncLocalStorage = new AsyncLocalStorage<RequestContext>();
export function correlationMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
const correlationId =
(req.headers["x-correlation-id"] as string) ?? randomUUID();
res.setHeader("X-Correlation-ID", correlationId);
asyncLocalStorage.run({ correlationId }, () => {
next();
});
}
```
```typescript
// logger.ts — augment the logger to include correlation ID
import { asyncLocalStorage } from "./middleware/correlation";
export function getContextLogger(): pino.Logger {
const store = asyncLocalStorage.getStore();
if (store) {
return logger.child({ correlationId: store.correlationId });
}
return logger;
}
```
```typescript
// Usage in any module
import { getContextLogger } from "./logger";
export async function getUser(userId: number): Promise<User> {
const log = getContextLogger();
log.info({ userId }, "fetching_user");
// Output includes correlationId automatically
return await userRepo.findById(userId);
}
```
### Propagating to downstream services
When calling other microservices, forward the correlation ID:
```typescript
import { asyncLocalStorage } from "./middleware/correlation";
export async function callBillingService(userId: number): Promise<Invoice[]> {
const store = asyncLocalStorage.getStore();
const response = await fetch(
`http://billing-service/api/v1/invoices?user_id=${userId}`,
{
headers: {
"X-Correlation-ID": store?.correlationId ?? "",
},
}
);
return response.json();
}
```
---
## 4. Sensitive Data Redaction
Pino has built-in redaction support for field paths.
```typescript
import pino from "pino";
export const logger = pino({
level: "info",
redact: {
paths: [
"password",
"secret",
"token",
"apiKey",
"authorization",
"creditCard",
"req.headers.authorization",
"req.headers.cookie",
"body.password",
"body.creditCardNumber",
"*.password",
"*.secret",
],
censor: "***REDACTED***",
},
});
```
For more complex redaction (regex-based), use a custom serializer:
```typescript
// redact.ts
const CREDIT_CARD_RE = /\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g;
const BEARER_RE = /Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi;
export function redactValue(value: unknown): unknown {
if (typeof value === "string") {
let result = value.replace(CREDIT_CARD_RE, "****-****-****-****");
result = result.replace(BEARER_RE, "Bearer ***REDACTED***");
return result;
}
if (typeof value === "object" && value !== null) {
return redactObject(value as Record<string, unknown>);
}
return value;
}
const SENSITIVE_KEYS =
/^(password|passwd|secret|token|api_?key|authorization|credit_?card|cvv|ssn)$/i;
function redactObject(obj: Record<string, unknown>): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
if (SENSITIVE_KEYS.test(key)) {
result[key] = "***REDACTED***";
} else {
result[key] = redactValue(value);
}
}
return result;
}
```
---
## 5. Request/Response Logging
Log every HTTP request and response with method, path, status code, duration, and body size. Uses Express middleware.
```typescript
// middleware/request-logging.ts
import type { Request, Response, NextFunction } from "express";
import { getContextLogger } from "../logger";
export function requestLogging(
req: Request,
res: Response,
next: NextFunction
): void {
const start = process.hrtime.bigint();
const log = getContextLogger();
log.info(
{
method: req.method,
path: req.originalUrl,
clientIp: req.ip,
userAgent: req.get("user-agent"),
},
"http_request_started"
);
// Hook into the response finish event
res.on("finish", () => {
const durationMs =
Number(process.hrtime.bigint() - start) / 1_000_000;
const logFn =
res.statusCode >= 500
? log.error.bind(log)
: res.statusCode >= 400
? log.warn.bind(log)
: log.info.bind(log);
logFn(
{
method: req.method,
path: req.originalUrl,
statusCode: res.statusCode,
durationMs: Math.round(durationMs * 100) / 100,
contentLength: res.get("content-length")
? parseInt(res.get("content-length")!, 10)
: undefined,
},
"http_request_completed"
);
});
next();
}
```
```typescript
// Register middleware (order matters)
app.use(correlationMiddleware);
app.use(requestLogging);
```
---
## 6. Error Logging
When logging errors, include the stack trace via pino's `err` serializer and enough context to reproduce the issue.
```typescript
import { getContextLogger } from "./logger";
const log = getContextLogger();
export async function processOrder(orderId: string): Promise<Order> {
log.info({ orderId }, "processing_order");
try {
const order = await orderRepo.findById(orderId);
if (!order) {
log.error({ orderId }, "order_not_found");
throw new OrderNotFoundError(orderId);
}
const payment = await paymentService.charge({
amount: order.total,
currency: order.currency,
customerId: order.customerId,
});
log.info(
{ orderId, paymentId: payment.id, amount: order.total },
"payment_processed"
);
return order;
} catch (err) {
if (err instanceof PaymentError) {
log.error(
{
orderId,
errorCode: err.code,
errorMessage: err.message,
err, // pino serializes Error objects with stack traces
},
"payment_failed"
);
} else {
log.error(
{
orderId,
err,
errorType: (err as Error).constructor.name,
},
"order_processing_unexpected_error"
);
}
throw err;
}
}
```
---
## 7. Performance Logging
### Timing wrapper
```typescript
import { createLogger } from "./logger";
const perfLog = createLogger("performance");
export async function withTiming<T>(
operation: string,
fn: () => Promise<T>,
slowThresholdMs = 1000
): Promise<T> {
const start = performance.now();
try {
const result = await fn();
return result;
} finally {
const durationMs = performance.now() - start;
const logFn = durationMs > slowThresholdMs ? perfLog.warn : perfLog.debug;
logFn(
{
operation,
durationMs: Math.round(durationMs * 100) / 100,
slow: durationMs > slowThresholdMs,
},
"operation_duration"
);
}
}
// Usage
const profile = await withTiming("fetch_user_profile", () =>
userRepo.getWithPreferences(userId)
);
```
+358
View File
@@ -0,0 +1,358 @@
---
name: openapi
description: Use when designing, documenting, or generating REST APIs with OpenAPI 3.1 — including error contracts, pagination, versioning, auth schemes, request/response schemas, webhooks, or code-gen pipelines for FastAPI, Express, or NestJS. Also use when migrating a spec from OpenAPI 3.0 to 3.1.
---
# OpenAPI 3.1 & REST API Design
## Overview
A design-first reference for REST APIs developers want to use. Standardizes on **RFC 9457 Problem Details**, **camelCase JSON**, **cursor pagination**, and **URL-path versioning** — the 2026 consensus across Google, Microsoft, and the OpenAPI 3.1 ecosystem.
## When to Use
- Designing or documenting a new REST API
- Generating clients/servers from a spec (FastAPI, Express, NestJS, etc.)
- Establishing error, pagination, versioning, or auth conventions for a service
- Migrating a spec from OpenAPI 3.0 → 3.1
- Setting up lint/governance in CI
## When NOT to Use
- GraphQL APIs (different spec format)
- Internal scripts or CLI tools with no HTTP surface
- RPC-style services (gRPC, tRPC)
---
## Quick Reference
| I need... | Go to |
|-----------|-------|
| Starter spec to copy and adapt | [templates/openapi-3.1-starter.yaml](templates/openapi-3.1-starter.yaml) |
| Which HTTP status code? | [references/http-status-codes.md](references/http-status-codes.md) |
| URL naming & CRUD mapping | [references/rest-naming.md](references/rest-naming.md) |
| Linting, CI, docs, client gen, mock servers | [references/api-governance.md](references/api-governance.md) |
| Idempotency, rate limiting, ETag, webhook signing, async jobs | [references/production-patterns.md](references/production-patterns.md) |
| Error contract (Problem Details) | § Errors below |
| Pagination pattern | § Pagination below |
| Auth scheme | § Authentication below |
| OpenAPI 3.0 → 3.1 gotchas | § Migration Flags below |
---
## Conventions — the defaults this skill teaches
Pick different conventions only with explicit reason, and apply them **consistently across the whole API**. Mixed conventions within one API are the #1 integration pain point.
| Decision | Default | Why |
|----------|---------|-----|
| Error format | `application/problem+json` (**RFC 9457**) | 2023 successor to RFC 7807. Standard fields, machine-readable `type` URI, native support in Spring / .NET / FastAPI. |
| JSON field casing | **camelCase** | Matches the JS/TS ecosystem (the largest API-consumer population) and Google / Microsoft guidelines. |
| URL segment casing | lowercase plural **kebab-case** (`/user-profiles`) | RFC 3986 friendly, cache-friendly, unambiguous. |
| Query parameter casing | **camelCase** (`pageSize`, `createdAfter`) | Mirrors the JSON body — consumers reason about one casing, not two. |
| HTTP header casing | `Kebab-Case` (`X-Request-Id`, `Idempotency-Key`) | HTTP convention. |
| Pagination | **Cursor** for growable lists; offset only for small bounded sets | Cursor is stable under concurrent inserts/deletes and O(1) per page. |
| Versioning (public) | URL path (`/v1`, `/v2`) | Explicit, routable, CDN/cache-friendly. |
| Versioning (internal) | Date header (`X-Api-Version: 2026-06-01`) | Fine-grained evolution without URL churn. |
| ID format in JSON | `string` — UUID or prefixed slug (`usr_abc123`) | Avoids JS number precision loss; prefixed IDs aid debugging. |
| Timestamps | ISO 8601 / RFC 3339 string | Universal, sortable, timezone-aware. |
---
## Spec Structure
Skeleton of a well-organized OpenAPI 3.1 document. Split large specs with `$ref` and bundle with `redocly cli bundle` for tooling.
```yaml
openapi: 3.1.0
info:
title: Acme API
version: 2.0.0
contact: { name: API Support, email: api@acme.dev }
license: { name: MIT, identifier: MIT } # 3.1 SPDX identifier
servers:
- url: https://api.acme.dev/v2
- url: https://staging-api.acme.dev/v2
tags:
- { name: Users, description: User management }
- { name: Orders, description: Order lifecycle }
paths:
/users: { $ref: './paths/users.yaml' }
/users/{userId}: { $ref: './paths/users-by-id.yaml' }
components:
schemas: { $ref: './components/schemas/_index.yaml' }
securitySchemes:
BearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
security:
- BearerAuth: []
webhooks:
orderCompleted: { $ref: './webhooks/order-completed.yaml' }
```
Recommended file layout:
```
spec/
├── openapi.yaml
├── paths/ # One file per resource
├── components/
│ └── schemas/ # Shared schemas
└── webhooks/ # Event payloads
```
For a complete runnable starter — CRUD, auth, cursor pagination, Problem Details, idempotency, rate-limit headers, and ETag concurrency — see [templates/openapi-3.1-starter.yaml](templates/openapi-3.1-starter.yaml).
---
## Path & Operation Patterns
- **Plural collections:** `/users`, `/orders`
- **Path params for identity:** `/users/{userId}`
- **Query params for filter / sort / paginate / expand**
- **Max 2 levels of nesting** — deeper is a smell, flatten it
- **Always set `operationId`** — code generators use it as the method name
- **Always set `tags` and `summary`** — docs UIs and linters require them
Full CRUD mapping table and URL rules: [references/rest-naming.md](references/rest-naming.md).
---
## Request Bodies
Use `additionalProperties: false` on request schemas so typos in payloads fail validation early instead of being silently dropped.
```yaml
CreateUserRequest:
type: object
required: [email, name]
additionalProperties: false
properties:
email: { type: string, format: email, maxLength: 254 }
name: { type: string, minLength: 1, maxLength: 100 }
role: { type: string, enum: [admin, member, viewer], default: member }
```
Framework mirrors:
- **FastAPI:** `pydantic.BaseModel` with `model_config = {"extra": "forbid"}`
- **Express + Zod:** `z.object({...}).strict()`
- **NestJS:** `class-validator` + `ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })`
For uploads, use `multipart/form-data` with `type: string, format: binary` and document the accepted MIME types via `encoding.<field>.contentType`.
---
## Errors (Problem Details)
Return `application/problem+json` per **RFC 9457** for every 4xx / 5xx response. Define one shared `ProblemDetails` schema and reuse it across the spec via `$ref`.
```yaml
components:
schemas:
ProblemDetails:
type: object
required: [type, title, status]
properties:
type:
type: string
format: uri
description: URI reference identifying the problem type.
example: https://api.acme.dev/problems/validation-error
title: { type: string, example: "Validation failed" }
status: { type: integer, example: 422 }
detail: { type: string, example: "Field 'email' must be a valid email address." }
instance: { type: string, format: uri }
errors:
type: array
description: Field-level validation errors (extension member).
items:
type: object
required: [field, message]
properties:
field: { type: string, example: email }
message: { type: string }
code: { type: string, example: invalidFormat }
responses:
ValidationError:
description: Request validation failed
content:
application/problem+json:
schema: { $ref: '#/components/schemas/ProblemDetails' }
```
**Make `type` a real documentation URL.** The whole point of RFC 9457 is that `type` uniquely identifies the problem class and links to a human explanation. Using `about:blank` (the spec default) throws away 90% of the value.
**400 vs 422 — the line that matters:**
- `400` → malformed syntax. The request body is not valid JSON, a required field is missing, or a field has the wrong type. Detected by the parser/validator.
- `422` → semantically valid but violates business rules. Email already exists, state transition illegal, quota exceeded. Detected by application logic.
Full status-code catalog and decision flow: [references/http-status-codes.md](references/http-status-codes.md).
---
## Authentication
Pick one scheme, apply globally via top-level `security`, override per operation as needed.
```yaml
components:
securitySchemes:
BearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
ApiKeyAuth: { type: apiKey, in: header, name: X-Api-Key }
OAuth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.acme.dev/authorize
tokenUrl: https://auth.acme.dev/token
scopes:
"users:read": Read user profiles
"users:write": Create and update users
security:
- BearerAuth: []
paths:
/health:
get:
security: [] # Public endpoint — override global
responses: { '200': { description: Healthy } }
/users:
get:
security:
- OAuth2: ["users:read"]
```
---
## Pagination
**Default to cursor pagination** for any list that can grow. It is stable under concurrent inserts/deletes and O(1) per page regardless of depth.
```yaml
components:
parameters:
Cursor:
name: cursor
in: query
description: Opaque cursor from a previous response.
schema: { type: string }
Limit:
name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
schemas:
UserListResponse:
type: object
required: [data, pagination]
properties:
data:
type: array
items: { $ref: '#/components/schemas/User' }
pagination:
type: object
required: [hasMore]
properties:
nextCursor: { type: [string, "null"] } # JSON Schema 2020-12 null union
hasMore: { type: boolean }
```
**Use offset pagination only** for small, bounded collections (< ~10k rows) where users need to jump to specific page numbers. Offset drifts when rows are inserted/deleted between requests and scales O(n) in the skipped-row count.
**Always enforce a max `limit`.** "We'll bound it later" never happens before the incident.
---
## Versioning
**Public APIs → URL path** (`/v1`, `/v2`). Simple, explicit, routable by CDN/gateway. This is the current mainstream choice (Google, Stripe, GitHub).
**Internal APIs → Date header** (`X-Api-Version: 2026-06-01`). Fine-grained, no URL churn, easier to deprecate individual fields.
Bump the major version when you break backward compatibility: remove a field, change a type, change an error shape. Everything additive (new fields, new endpoints) stays on the same version.
**Never ship without a version.** Adding one later is painful.
---
## Webhooks
OpenAPI 3.1 introduces top-level `webhooks` for outbound events — a first-class replacement for the old `callbacks` workaround.
```yaml
webhooks:
orderCompleted:
post:
operationId: onOrderCompleted
summary: Fired when an order reaches "completed" state.
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/OrderCompletedEvent' }
responses:
'2XX': { description: Webhook received successfully. }
components:
schemas:
WebhookEventBase:
type: object
required: [id, type, createdAt]
properties:
id: { type: string, format: uuid }
type: { type: string }
createdAt: { type: string, format: date-time }
OrderCompletedEvent:
allOf:
- $ref: '#/components/schemas/WebhookEventBase'
- type: object
properties:
type: { const: order.completed }
data:
type: object
properties:
orderId: { type: string, format: uuid }
total: { type: number, format: double }
```
Consumers should respond with any 2xx within a few seconds; you retry non-2xx with exponential backoff. **Sign payloads** (HMAC over timestamp + body) so consumers can verify authenticity and reject replays — full pattern with working signing/verification code in [references/production-patterns.md](references/production-patterns.md).
---
## OpenAPI 3.0 → 3.1 Migration Flags
The most common mistakes when moving a spec from 3.0 to 3.1:
| 3.0 (wrong in 3.1) | 3.1 (correct) |
|--------------------|---------------|
| `nullable: true` | `type: [string, "null"]``nullable` is **silently ignored** in 3.1 |
| `example: ...` on schema | `examples: [...]` — now an array |
| `exclusiveMinimum: true` + `minimum: 0` | `exclusiveMinimum: 0` — now a numeric value |
| `$ref` cannot have sibling keywords | `$ref` can sit next to `description`, `summary`, etc. |
| `type: integer, format: int64` for long IDs | `type: string` — JS `Number` loses precision above 2^53 |
3.1 is full **JSON Schema 2020-12**: use `oneOf` + `discriminator` for polymorphism, and `if` / `then` / `else` for conditional validation.
---
## Common Pitfalls
1. **Undocumented error responses.** Every operation should list its possible 4xx/5xx codes. At minimum: `400`, `401`, `403`, `404`, `422`, `500`. Consumers cannot handle errors they don't know about.
2. **Inline schemas instead of `$ref`.** Kills SDK quality — generators produce names like `UsersPost200Response`. Put shared shapes under `components/schemas`.
3. **Missing `operationId`.** Generators fall back to `userGet1`, `userGet2`. Every operation needs an `operationId`.
4. **Unbounded list endpoints.** No `limit` cap = OOM or timeout the day traffic doubles.
5. **Mixed casing.** camelCase and snake_case inside the same API confuses every consumer. Enforce with `spectral` or `vacuum`.
6. **`nullable: true` in a 3.1 document.** Silently ignored. Use the JSON Schema null union (`type: [T, "null"]`).
7. **Ambiguous `oneOf` without `discriminator`.** Clients cannot reliably deserialize polymorphic payloads.
8. **Deeply nested URLs.** `/users/{uid}/orders/{oid}/items/{iid}/notes` is fragile and uncacheable. Flatten once the parent relationship is established.
9. **Using `about:blank` for Problem Details `type`.** Wastes the main benefit of RFC 9457 — link to your real docs.
10. **No linter in CI.** Specs drift. Run `redocly lint`, `spectral lint`, or `vacuum` on every PR from day one. For a ready-to-copy Spectral ruleset, GitHub Actions workflow, and a breaking-change diff step, see [references/api-governance.md](references/api-governance.md).
11. **No idempotency on side-effecting POSTs.** A lost response = duplicate charge / duplicate email. Accept an `Idempotency-Key` header and store the *response* keyed by it. Full pattern: [references/production-patterns.md](references/production-patterns.md).
---
## Related Skills
- `api-client` — consuming and generating API clients from specs
- `error-handling` — consistent error handling in consumer code
- `fastapi` — FastAPI's built-in OpenAPI generation
@@ -0,0 +1,274 @@
# API Governance & Tooling
Linters, docs generators, client generators, mock servers, and contract testing tools for an OpenAPI-based workflow. Focus on what is actually used in 2026.
---
## Why governance matters
Specs drift. A hand-written spec that is not linted in CI will accumulate missing `operationId`s, inline schemas, undocumented errors, and inconsistent naming within weeks. A generated spec (from FastAPI/NestJS) will drift toward whatever the framework emits, which is rarely idiomatic.
**Minimum bar for any new API:**
1. Spec lives in version control
2. A linter runs on every PR
3. Docs regenerate on merge to main
4. At least one generated client (or contract test) catches breaking changes
---
## Linters
All three below consume the same Spectral-compatible rule format.
| Tool | Language | Speed | When to pick |
|------|----------|-------|--------------|
| **Spectral** (Stoplight) | Node | Moderate | Default choice. Largest rule ecosystem, first-party Redocly + Zalando rulesets. |
| **Redocly CLI** | Node | Moderate | Pick if you also want `bundle`, `split`, and `build-docs` in one tool. Ships its own opinionated ruleset. |
| **Vacuum** (daveshanley) | Go | 1020× faster | Pick for large specs (500+ operations) or monorepo CI where seconds matter. Drop-in Spectral rule compatibility. |
### Minimum Spectral ruleset
Save as `.spectral.yaml` in the spec directory:
```yaml
extends:
- spectral:oas # Base OpenAPI rules
- spectral:asyncapi # If you mix in AsyncAPI
rules:
# Every operation must be identified, tagged, and summarized
operation-operationId: error
operation-operationId-unique: error
operation-tags: error
operation-summary: error
operation-description: warn
# Schemas must be referenced, not inlined
no-inline-schemas:
description: Request/response bodies must $ref a named schema.
severity: error
given: "$.paths.*.*.requestBody.content.*.schema"
then:
field: "$ref"
function: truthy
# No 3.0-isms in a 3.1 document
no-nullable:
description: "Use type: [T, 'null'] instead of nullable: true in 3.1."
severity: error
given: "$..nullable"
then:
function: falsy
# Enforce camelCase on JSON properties
camel-case-properties:
description: Property names must be camelCase.
severity: error
given: "$.components.schemas..properties[*]~"
then:
function: pattern
functionOptions:
match: "^[a-z][a-zA-Z0-9]*$"
# Every operation declares at least one 4xx and one 5xx response
operation-4xx-response:
severity: error
given: "$.paths.*.*.responses"
then:
function: schema
functionOptions:
schema:
type: object
patternProperties:
"^4\\d\\d$": {}
minProperties: 1
# Error bodies use application/problem+json
error-uses-problem-json:
description: 4xx/5xx responses must use application/problem+json.
severity: warn
given: "$.paths.*.*.responses[?(@property.match(/^[45]\\d\\d$/))].content"
then:
field: "application/problem+json"
function: truthy
```
### GitHub Actions CI snippet
```yaml
# .github/workflows/api-spec.yml
name: API spec
on:
pull_request:
paths: ['spec/**']
push:
branches: [main]
paths: ['spec/**']
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- name: Bundle spec
run: npx @redocly/cli@latest bundle spec/openapi.yaml -o spec/openapi.bundled.yaml
- name: Lint with Spectral
run: npx @stoplight/spectral-cli@latest lint spec/openapi.bundled.yaml --fail-severity=error
- name: Detect breaking changes vs main
if: github.event_name == 'pull_request'
run: |
git fetch origin main
npx @redocly/cli@latest diff origin/main:spec/openapi.yaml spec/openapi.yaml --fail-on=breaking
```
The `diff --fail-on=breaking` step blocks PRs that remove fields, change types, or rename operations — the most common accidental breakages.
---
## Docs Generators
| Tool | Style | When to pick |
|------|-------|--------------|
| **Scalar** | Modern three-column with built-in REST client | Default choice for new projects. Fast, polished, open source. |
| **Redoc** | Classic three-column reference (Stripe-like) | Pick when you want the most battle-tested static docs. Works offline. |
| **Redocly Portal** | Hosted docs with analytics, try-it, versioning | Pick when you need a revenue-class docs portal. Paid. |
| **Swagger UI** | Interactive try-it | Pick only for internal/debug dashboards. Aesthetics lag behind Scalar/Redoc. |
### Scalar (recommended default)
```html
<!-- docs.html -->
<!doctype html>
<html>
<head><title>My API</title></head>
<body>
<script id="api-reference" data-url="/openapi.yaml"></script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>
```
### Redoc (static build)
```bash
npx @redocly/cli build-docs spec/openapi.yaml -o dist/api.html
```
Deploy the single HTML file to any static host (Cloudflare Pages, S3, GitHub Pages).
---
## Client Generation
| Tool | Targets | When to pick |
|------|---------|--------------|
| **Kubb** | TypeScript (Zod, TanStack Query, SWR, MSW, Axios, Fetch) | Default for 2026 frontend. Plugin-based, generates exactly what you want, no framework bloat. |
| **Orval** | TypeScript (React Query, SWR, Zod, MSW, Axios) | Close alternative to Kubb. Pick if you prefer a single-config approach. |
| **openapi-generator** | 30+ languages (Python, Go, Java, Kotlin, Ruby, Rust, etc.) | Default for non-TypeScript languages. The workhorse, but generated code is heavier. |
| **openapi-ts** (hey-api) | TypeScript only, lightweight | Pick when you want a minimal fetch wrapper with full types and zero framework coupling. |
### Kubb starter (TypeScript + TanStack Query + Zod)
```ts
// kubb.config.ts
import { defineConfig } from '@kubb/core'
import { pluginOas } from '@kubb/plugin-oas'
import { pluginTs } from '@kubb/plugin-ts'
import { pluginZod } from '@kubb/plugin-zod'
import { pluginClient } from '@kubb/plugin-client'
import { pluginReactQuery } from '@kubb/plugin-react-query'
export default defineConfig({
input: { path: './spec/openapi.yaml' },
output: { path: './src/api/generated', clean: true },
plugins: [
pluginOas(),
pluginTs(),
pluginZod(),
pluginClient({ importPath: '../client.ts' }),
pluginReactQuery(),
],
})
```
### openapi-generator (Python / Go / Java / etc.)
```bash
npx @openapitools/openapi-generator-cli generate \
-i spec/openapi.yaml \
-g python \
-o clients/python \
--additional-properties=packageName=acme_client,library=asyncio
```
---
## Mock Servers
| Tool | When to pick |
|------|--------------|
| **Prism** (Stoplight) | Run a mock server directly from your spec. Validates requests against the schema and returns examples. Best for frontend dev against an unfinished backend. |
| **MSW** (Mock Service Worker) | Runs in the browser/Node for testing client code. Pair with Kubb's `@kubb/plugin-msw` to generate handlers from the spec. |
### Prism starter
```bash
npx @stoplight/prism-cli mock spec/openapi.yaml --port 4010
# Server at http://127.0.0.1:4010 responds based on the spec examples
```
Add `--errors` to make Prism return the declared error responses when the request is invalid, useful for exercising error paths.
---
## Contract Testing
Tools that verify the running implementation still matches the spec.
| Tool | Approach | When to pick |
|------|----------|--------------|
| **Schemathesis** | Property-based fuzzing driven by the spec | Best signal per line of setup. Catches unhandled edge cases the developer never thought to test. |
| **Dredd** | Replays documented examples against the server | Simple smoke-test. Good for regression on happy paths. |
| **Pact** | Consumer-driven contracts (not spec-first) | Pick when consumers write the contracts rather than deriving from the server's OpenAPI. |
### Schemathesis in CI
```bash
pipx install schemathesis
schemathesis run spec/openapi.yaml \
--base-url=http://localhost:3000/v1 \
--checks=all \
--hypothesis-max-examples=50
```
Runs ~50 generated requests per operation and checks: status code validity, response schema conformance, `Content-Type` match, and absence of server errors (5xx).
---
## Governance checklist
Before calling an API "production-ready":
- [ ] Spec is in version control alongside the code
- [ ] Spec is bundled (`redocly bundle`) and the bundled artifact is linted
- [ ] Spectral (or equivalent) runs on every PR and blocks on errors
- [ ] A breaking-change check runs on every PR (`redocly diff` or `oasdiff`)
- [ ] Every operation has `operationId`, `tags`, `summary`, at least one `4xx` and at least one `5xx` response
- [ ] Docs are regenerated on merge to main (Scalar, Redoc, or portal)
- [ ] At least one generated client is compiled in CI — proves the spec is consumable
- [ ] Contract tests run against a deployed preview before merge
- [ ] A mock server (Prism) is available for consumer development
---
## Related
- [rest-naming.md](rest-naming.md) — URL and naming conventions
- [http-status-codes.md](http-status-codes.md) — status code selection
- [production-patterns.md](production-patterns.md) — idempotency, rate limiting, ETags, webhook signing
- [openapi.tools](https://openapi.tools/) — community catalog of all OpenAPI tools
@@ -24,9 +24,9 @@ Quick reference for selecting the correct HTTP status code in REST API responses
{
"id": "usr_abc123",
"name": "Jane Doe",
"created_at": "2025-01-15T10:30:00Z"
"createdAt": "2026-01-15T10:30:00Z"
}
// Header: Location: /api/v1/users/usr_abc123
// Header: Location: /v1/users/usr_abc123
```
---
@@ -64,34 +64,41 @@ Quick reference for selecting the correct HTTP status code in REST API responses
| `429` | Too Many Requests | Rate limit exceeded. Include `Retry-After` header. |
**Guidelines:**
- `400` for structural issues (bad JSON, missing required fields)
- `422` for business logic failures (email already taken, invalid state transition)
- `401` means "who are you?" -- `403` means "I know who you are, but no"
- `409` for optimistic locking failures and unique constraint violations
- `429` must include `Retry-After` header with seconds until retry
- `400` for **structural** issues (bad JSON, missing required field, wrong type) — caught by the parser/validator.
- `422` for **semantic** failures (email already taken, invalid state transition, quota exceeded) — caught by application logic.
- `401` means "who are you?" `403` means "I know who you are, but no".
- `409` for optimistic-locking failures and unique-constraint violations.
- `412` for `If-Match` / `If-Unmodified-Since` precondition failures (ETag concurrency).
- `429` must include `Retry-After` with seconds until retry.
**400 vs 422 decision rule:** If the request body failed to parse or a required field is missing, return `400`. If the body parsed fine and every field has the right type but the combination violates a business rule, return `422`.
All error bodies use `application/problem+json` per **RFC 9457**.
```json
// 422 Unprocessable Entity
// Content-Type: application/problem+json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{ "field": "email", "message": "Email already registered" },
{ "field": "age", "message": "Must be 18 or older" }
"type": "https://api.example.com/problems/validation-error",
"title": "Validation failed",
"status": 422,
"detail": "Request validation failed.",
"errors": [
{ "field": "email", "message": "Email already registered", "code": "conflict" },
{ "field": "age", "message": "Must be 18 or older", "code": "outOfRange" }
]
}
}
```
```json
// 429 Too Many Requests
// Header: Retry-After: 60
// Headers: Retry-After: 60
// Content-Type: application/problem+json
{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Try again in 60 seconds."
}
"type": "https://api.example.com/problems/rate-limited",
"title": "Too many requests",
"status": 429,
"detail": "Rate limit exceeded. Retry after 60s."
}
```
@@ -115,15 +122,19 @@ Quick reference for selecting the correct HTTP status code in REST API responses
```json
// 500 Internal Server Error (production)
// Content-Type: application/problem+json
{
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred. Please try again.",
"request_id": "req_7f3a9b2c"
}
"type": "https://api.example.com/problems/internal-error",
"title": "Internal server error",
"status": 500,
"detail": "An unexpected error occurred. Please try again.",
"instance": "/v1/users/usr_abc123",
"requestId": "req_7f3a9b2c"
}
```
Extension members (`requestId`, `traceId`, etc.) are encouraged by RFC 9457 — include anything that helps the caller report the bug.
---
## Decision Flowchart
@@ -141,6 +152,8 @@ Request received
|
+-- Is it rate-limited? -- YES --> 429 Too Many Requests
|
+-- If-Match / If-Unmodified-Since fails? -- YES --> 412 Precondition Failed
|
+-- Does it pass business rules? -- NO --> 422 Unprocessable Entity
|
+-- Any conflicts? -- YES --> 409 Conflict
@@ -157,19 +170,22 @@ Request received
---
## Standard Error Response Format
## Standard Error Response Format — RFC 9457 Problem Details
Use a consistent structure across all error responses:
Every error response uses the `application/problem+json` media type with this shape:
```json
{
"error": {
"code": "MACHINE_READABLE_CODE",
"message": "Human-readable description",
"details": [],
"request_id": "req_..."
}
"type": "https://api.example.com/problems/<problem-slug>",
"title": "Short human-readable summary",
"status": 422,
"detail": "Human-readable explanation for this occurrence.",
"instance": "/v1/users/usr_abc123",
"errors": [ /* optional: field-level validation breakdown */ ],
"requestId": "req_..."
}
```
*Reference: [RFC 9110 - HTTP Semantics](https://httpwg.org/specs/rfc9110.html), [RFC 9457 - Problem Details](https://www.rfc-editor.org/rfc/rfc9457)*
Required fields: `type`, `title`, `status`. Everything else is optional but strongly recommended. The `type` URI should resolve to a real documentation page — that is the core benefit of RFC 9457 over ad-hoc envelopes.
*Reference: [RFC 9110 — HTTP Semantics](https://httpwg.org/specs/rfc9110.html), [RFC 9457 — Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457)*
@@ -0,0 +1,417 @@
# Production-Grade API Patterns
Four patterns that separate hobby APIs from APIs developers actually trust in production: **idempotency keys**, **rate limiting**, **optimistic concurrency (ETag)**, and **webhook signing**. Plus the **async/202** pattern for long-running work.
Each section has: what it is, why it matters, the HTTP contract, and server-side pseudocode.
---
## 1. Idempotency Keys
**Problem.** A client calls `POST /payments` to charge $50. The request succeeds on the server but the response is lost to a network blip. The client retries. Without idempotency, the customer is charged twice.
**Solution.** The client generates a UUID per logical operation and sends it as an `Idempotency-Key` header. The server stores the full response keyed by `(idempotencyKey, userId)` for a TTL window (Stripe uses 24h). Any retry with the same key returns the stored response — no re-execution.
**Key insight — store the *response*, not the request.** Replaying the operation on retry defeats the purpose. You must serve the exact bytes the original call produced, including the status code, headers, and body. Otherwise a second worker racing the first will see inconsistent state.
### HTTP contract
```
POST /v1/payments
Idempotency-Key: 0f6f7a7d-1c6a-4a1e-9d1a-4f2a1b3c4d5e
Authorization: Bearer <token>
Content-Type: application/json
{ "amount": 5000, "currency": "usd", "customerId": "cus_abc" }
```
Server responses:
- **First call:** process normally, store `(key, response)`, return `201 Created` + `Payment` body.
- **Retry (same key, same body):** return the stored response verbatim. Include header `Idempotent-Replayed: true` so clients can log the replay.
- **Retry (same key, different body):** return `422 Unprocessable Entity` with `type: .../problems/idempotency-conflict`. The key was reused for a different payload — that is a client bug.
- **Retry during in-flight processing:** return `409 Conflict` so the client backs off and retries later. Acquire a lock on `(key)` before starting work.
### Server-side pseudocode
```python
async def create_payment(req: Request, key: str | None, user: User):
if key is None:
# Idempotency optional, but log the risk.
return await process_payment(req, user)
record = await idempotency_store.get(key, user.id)
if record:
if record.request_hash != hash(req.body):
raise ProblemDetail(422, "idempotency-conflict",
"Key reused with a different request body.")
if record.status == "in_progress":
raise ProblemDetail(409, "idempotency-in-progress",
"Original request still processing.")
return replay(record.response) # exact bytes
# Claim the key atomically — losing this race returns 409
claimed = await idempotency_store.claim(key, user.id, hash(req.body))
if not claimed:
raise ProblemDetail(409, "idempotency-in-progress",
"Original request still processing.")
try:
response = await process_payment(req, user)
await idempotency_store.save(key, user.id, response, ttl=24h)
return response
except Exception as e:
# Release the claim so the client can retry cleanly.
await idempotency_store.release(key, user.id)
raise
```
**Storage:** Redis with a 24h TTL is the standard choice. Use a Redis transaction (`WATCH`/`MULTI`) or `SETNX` for the claim step to avoid races.
**Scope:** key by `(idempotencyKey, apiKeyId)` so keys from one tenant never collide with another.
**Apply to:** all `POST` and `PATCH` that create or mutate resources with side effects (billing, emails, external API calls). Pure `GET`/`HEAD` is already idempotent; `PUT` and `DELETE` are idempotent by HTTP semantics but still benefit from replay protection for in-flight retries.
---
## 2. Rate Limiting
**Problem.** One misbehaving client floods your API and degrades everyone else. Without limits, a single bug can take the service down.
**Solution.** Bound requests per client per time window, return `429 Too Many Requests` when exceeded, and publish headers on every response so well-behaved clients can self-throttle before they hit the wall.
### Algorithms
| Algorithm | Burst behavior | Memory per key | When to pick |
|-----------|---------------|----------------|--------------|
| **Fixed window** | Allows 2× burst at window boundary | O(1) | Simple, cheap. Acceptable for soft limits. |
| **Sliding window log** | Smooth | O(n) per key | Expensive but precise. Use for billing-grade metering. |
| **Sliding window counter** | Near-smooth | O(1) | **Default choice.** Good accuracy, constant memory. |
| **Token bucket** | Configurable burst | O(1) | Use when you want to allow small bursts but bound sustained rate. Common for customer-facing APIs. |
Redis + sliding-window-counter is the pragmatic default.
### HTTP contract
**Every successful response:**
```
X-RateLimit-Limit: 1000 # quota for this window
X-RateLimit-Remaining: 942 # how many calls left
X-RateLimit-Reset: 1767225600 # unix seconds when the window resets
```
**429 response:**
```
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 60
{
"type": "https://api.example.com/problems/rate-limited",
"title": "Too many requests",
"status": 429,
"detail": "Rate limit of 1000/hour exceeded. Retry after 60s."
}
```
**`Retry-After` is mandatory on 429.** Clients use it to schedule the retry. Use seconds (integer) rather than HTTP-date — simpler, no clock-skew bugs.
### Server-side pseudocode (Redis sliding-window counter)
```python
# Pseudocode — use a battle-tested library (redis-rate-limit, slowapi, limits)
# rather than hand-rolling this in production.
async def enforce_rate_limit(key: str, limit: int, window_seconds: int) -> RateLimitResult:
now = int(time.time())
window_start = now - (now % window_seconds)
prev_window_start = window_start - window_seconds
# Atomic increment + get prior-window count
with redis.pipeline(transaction=True) as p:
p.incr(f"rl:{key}:{window_start}")
p.expire(f"rl:{key}:{window_start}", window_seconds * 2)
p.get(f"rl:{key}:{prev_window_start}")
curr, _, prev = p.execute()
# Weight the prior window by how much of the current window has elapsed
elapsed_fraction = (now % window_seconds) / window_seconds
weighted = int((int(prev or 0)) * (1 - elapsed_fraction)) + int(curr)
remaining = max(0, limit - weighted)
reset_at = window_start + window_seconds
if weighted > limit:
return RateLimitResult(
allowed=False,
retry_after=reset_at - now,
limit=limit, remaining=0, reset_at=reset_at,
)
return RateLimitResult(
allowed=True,
limit=limit, remaining=remaining, reset_at=reset_at,
)
```
**Key strategy:** by authenticated principal (`userId` or `apiKeyId`), not by IP. IP-based limiting punishes users behind corporate NATs.
**Tiers:** different limits for anonymous / free / paid is common. Store the tier on the auth token and look up the limit at enforcement time — don't hard-code.
**Where to enforce:** at the edge (gateway/middleware) before hitting your application logic. An API gateway (Kong, Envoy, Cloudflare) handles this natively if you use one.
---
## 3. Optimistic Concurrency (ETag + If-Match)
**Problem.** Alice and Bob both load the same user record. Alice updates the email, Bob updates the name. If both `PATCH` without coordination, the second write silently overwrites the first ("lost update").
**Solution.** On `GET`, return an `ETag` header — an opaque version token. On `PATCH`, require the client to echo that ETag in `If-Match`. If the server's current ETag doesn't match, return `412 Precondition Failed` and the client must refetch and retry.
### ETag generation strategies
| Strategy | Pros | Cons |
|----------|------|------|
| Version counter (`v42`) | Trivial to compare | Needs a `version` column on every row |
| `updated_at` timestamp | No schema change | Millisecond precision may collide on bulk updates |
| Hash of body (`"a1b2c3"`) | Stateless | Recomputed on every GET |
| Database row version | Cheap, natural fit | ORM-dependent |
All work. Pick whatever matches your data layer.
**Weak vs strong:** `ETag: W/"..."` for weak (semantically equivalent), `ETag: "..."` for strong (byte-identical). Use strong ETags unless you need to support content negotiation.
### HTTP contract
```
GET /v1/users/usr_abc123 HTTP/1.1
Authorization: Bearer ...
HTTP/1.1 200 OK
ETag: "v42"
Content-Type: application/json
{ "id": "usr_abc123", "name": "Alice", "email": "alice@example.com", ... }
```
```
PATCH /v1/users/usr_abc123 HTTP/1.1
If-Match: "v42"
Content-Type: application/json
{ "email": "alice@new.example.com" }
```
**Success:**
```
HTTP/1.1 200 OK
ETag: "v43"
{ ... updated user ... }
```
**Conflict — Bob's write races Alice's:**
```
HTTP/1.1 412 Precondition Failed
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/precondition-failed",
"title": "Precondition failed",
"status": 412,
"detail": "The resource was modified since you last fetched it. Re-fetch and retry."
}
```
### Server-side pseudocode
```python
async def update_user(user_id: str, body: dict, if_match: str | None):
current = await users.get(user_id)
if current is None:
raise ProblemDetail(404, "not-found", f"User '{user_id}' not found.")
if if_match is None:
raise ProblemDetail(428, "precondition-required",
"If-Match header is required for this operation.")
current_etag = f'"v{current.version}"'
if if_match != current_etag:
raise ProblemDetail(412, "precondition-failed",
"The resource was modified since you last fetched it.")
updated = await users.patch(user_id, body, expected_version=current.version)
return updated, f'"v{updated.version}"'
```
**`428 Precondition Required`** is the correct response when the server *requires* `If-Match` and the client didn't send one. RFC 6585.
**Also useful for GETs:** `If-None-Match: "v42"` lets clients skip the body and get `304 Not Modified` if nothing changed — cheap cache revalidation.
---
## 4. Webhook Signing (HMAC)
**Problem.** You deliver an event to a consumer URL. Without a signature, anyone who guesses the URL can forge events. Without a timestamp, an attacker who captures one valid payload can replay it forever.
**Solution.** Sign every webhook with HMAC-SHA256 over `timestamp + "." + body`, send both in a header, and require consumers to reject signatures older than 5 minutes.
### HTTP contract
```
POST https://consumer.example.com/webhooks/acme HTTP/1.1
Content-Type: application/json
Acme-Signature: t=1767225600,v1=5257a869e7...7f4b
Acme-Webhook-Id: evt_01HTZ4K5M8N9P0Q1R2S3T4V5W6
{
"id": "evt_01HTZ4K5M8N9P0Q1R2S3T4V5W6",
"type": "order.completed",
"createdAt": "2026-04-15T10:30:00Z",
"data": { "orderId": "ord_xyz", "total": 4999, "currency": "usd" }
}
```
**`t=` is the unix timestamp when the signature was generated.**
**`v1=` is the hex-encoded HMAC-SHA256 of `t + "." + rawBody` using the consumer's signing secret.**
The version prefix (`v1=`) lets you rotate signing schemes in the future without breaking existing consumers.
### Server-side signing
```python
import hmac, hashlib, time, json
def sign_webhook(raw_body: bytes, secret: str) -> str:
t = int(time.time())
payload = f"{t}.".encode() + raw_body
sig = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return f"t={t},v1={sig}"
async def deliver(endpoint: Endpoint, event: dict):
raw = json.dumps(event, separators=(",", ":")).encode()
signature = sign_webhook(raw, endpoint.signing_secret)
await http.post(
endpoint.url,
content=raw,
headers={
"Content-Type": "application/json",
"Acme-Signature": signature,
"Acme-Webhook-Id": event["id"],
},
)
```
### Consumer-side verification (for your docs)
```python
import hmac, hashlib, time
MAX_AGE_SECONDS = 300 # 5 minutes
def verify_webhook(raw_body: bytes, header: str, secret: str) -> dict:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts["t"])
sig = parts["v1"]
# Replay protection — reject anything older than MAX_AGE
if abs(time.time() - t) > MAX_AGE_SECONDS:
raise SignatureError("Timestamp outside tolerance window.")
expected = hmac.new(
secret.encode(),
f"{t}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
# Constant-time compare — prevents timing attacks
if not hmac.compare_digest(expected, sig):
raise SignatureError("Signature mismatch.")
return json.loads(raw_body)
```
**Three non-negotiables:**
1. **Sign `timestamp + body`, not just body.** Without the timestamp, replay protection is impossible.
2. **Use constant-time comparison (`hmac.compare_digest`).** Never `==`. Side-channel leaks.
3. **Verify against the raw body bytes**, not a parsed-and-reserialized version. JSON serializers don't roundtrip byte-for-byte.
### Retry and dedup
- Retry on any non-2xx response with exponential backoff: 1m, 5m, 15m, 1h, 6h, 24h (cap at ~24h total).
- Include a unique event `id` in every payload; consumers must dedupe on it (retries will re-send the same `id`).
- Consumer must respond within ~5s with any 2xx. Do the actual work in a background job.
---
## 5. Async Long-Running Operations (202 Accepted)
**Problem.** Generating a report takes 30 seconds. You can't hold an HTTP connection that long — load balancers kill it, clients time out.
**Solution.** Return `202 Accepted` immediately with a `Location` header pointing to a status resource. The client polls (or subscribes to a webhook) until the job completes.
### HTTP contract
```
POST /v1/reports HTTP/1.1
Content-Type: application/json
{ "type": "sales", "startDate": "2026-01-01", "endDate": "2026-03-31" }
```
```
HTTP/1.1 202 Accepted
Location: /v1/jobs/job_01HTZ9K5M8N9P0Q1R2S3T4V5W6
Retry-After: 5
{
"id": "job_01HTZ9K5M8N9P0Q1R2S3T4V5W6",
"status": "queued",
"createdAt": "2026-04-15T10:30:00Z"
}
```
```
GET /v1/jobs/job_01HTZ9K5M8N9P0Q1R2S3T4V5W6 HTTP/1.1
HTTP/1.1 200 OK
{
"id": "job_01HTZ9K5M8N9P0Q1R2S3T4V5W6",
"status": "completed",
"createdAt": "2026-04-15T10:30:00Z",
"completedAt":"2026-04-15T10:30:32Z",
"result": { "reportUrl": "https://cdn.example.com/reports/xyz.csv" }
}
```
**Job states:** `queued``running``completed` | `failed` | `cancelled`.
On `failed`, embed a `ProblemDetails` object in the job body under `error` so the client gets structured failure info without a separate endpoint.
**Webhook option:** let the client register a callback URL on the job creation (`"callbackUrl": "https://..."`) and deliver a webhook when the job terminates, following the signing pattern above. Saves polling and is what mature APIs offer as the default.
---
## Applying this in OpenAPI
The starter template [openapi-3.1-starter.yaml](../templates/openapi-3.1-starter.yaml) already demonstrates four of these patterns on the `/users` endpoints:
| Pattern | Where in the template |
|---------|----------------------|
| Idempotency keys | `POST /users``IdempotencyKeyHeader` parameter |
| Rate limit headers | `GET /users` responses → `X-RateLimit-*` header refs |
| ETag + If-Match | `GET` + `PATCH /users/{userId}``ETag` header, `If-Match` param, `412` response |
| Problem Details errors | All `4xx`/`5xx` responses use `application/problem+json` |
Copy the relevant `parameters`, `headers`, and `responses` blocks into your own spec.
---
## Related
- [http-status-codes.md](http-status-codes.md) — 202, 409, 412, 428, 429 selection rules
- [rest-naming.md](rest-naming.md) — URL conventions
- [api-governance.md](api-governance.md) — linting, docs, client gen, contract testing
- [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) — Problem Details for HTTP APIs
- [Stripe: Designing APIs with Idempotency](https://stripe.com/blog/idempotency) — the canonical write-up
@@ -7,10 +7,11 @@ Guidelines for consistent, predictable REST endpoint design.
## Core Rules
1. **Use plural nouns** for resource collections
2. **Use kebab-case** for multi-word resources
2. **Use kebab-case** for multi-word URL segments (`/user-profiles`)
3. **Use path parameters** for identity, query parameters for filtering
4. **Never use verbs** in URLs (HTTP methods convey the action)
5. **Use lowercase** exclusively
4. **Never use verbs** in URLs HTTP methods convey the action
5. **Use lowercase** in URL path segments
6. **Use camelCase** in JSON bodies and query parameter names (`pageSize`, `createdAfter`) — matches the JS/TS ecosystem
---
@@ -45,7 +46,8 @@ GET /getUsers # verb in URL
GET /user/123 # singular collection
GET /Users # uppercase
POST /users/create # redundant verb
GET /user_profiles # snake_case
GET /user_profiles # snake_case (use kebab-case in paths)
GET /userProfiles # camelCase (use kebab-case in paths)
DELETE /users/123/delete # verb in URL
```
@@ -105,13 +107,13 @@ GET /reviews?order_item_id={iid}
```
GET /products?category=electronics&brand=acme
GET /users?status=active&role=admin
GET /orders?created_after=2025-01-01&created_before=2025-02-01
GET /orders?createdAfter=2026-01-01&createdBefore=2026-02-01
```
| Convention | Example |
|-----------|---------|
| Exact match | `?status=active` |
| Date range | `?created_after=2025-01-01` |
| Date range | `?createdAfter=2026-01-01` |
| Multiple values | `?status=active,pending` |
| Search | `?q=search+term` |
@@ -120,29 +122,43 @@ GET /orders?created_after=2025-01-01&created_before=2025-02-01
```
GET /products?sort=price # ascending (default)
GET /products?sort=-price # descending (prefix -)
GET /products?sort=-created_at,name # multi-field
GET /products?sort=-createdAt,name # multi-field
```
### Pagination
```
# Offset-based (simple, common)
GET /products?page=2&per_page=25
Prefer **cursor-based** for any list that can grow — it is stable under concurrent inserts/deletes and O(1) per page. Use **offset-based** only for small, bounded collections where users need to jump to a specific page number.
# Cursor-based (better for large datasets)
```
# Cursor-based (recommended default)
GET /products?cursor=eyJpZCI6MTAwfQ&limit=25
# Offset-based (only for bounded lists)
GET /products?page=2&limit=25
```
**Response envelope for paginated results:**
**Response envelope — cursor:**
```json
{
"data": [...],
"data": [ /* ... */ ],
"pagination": {
"nextCursor": "eyJpZCI6MTI1fQ",
"hasMore": true
}
}
```
**Response envelope — offset:**
```json
{
"data": [ /* ... */ ],
"pagination": {
"page": 2,
"per_page": 25,
"limit": 25,
"total": 150,
"total_pages": 6
"totalPages": 6
}
}
```
@@ -184,13 +200,15 @@ Some operations do not map cleanly to CRUD. Use sub-resources with a noun or POS
## Summary Checklist
- [ ] Resources are plural nouns (`/users` not `/user`)
- [ ] URLs are kebab-case and lowercase
- [ ] URL path segments are lowercase kebab-case
- [ ] JSON fields and query params are camelCase
- [ ] No verbs in URLs
- [ ] Nesting limited to 2 levels
- [ ] Filtering uses query parameters
- [ ] Sorting supports `-field` for descending
- [ ] Pagination included on all list endpoints
- [ ] Cursor pagination on any list that can grow; offset only for small bounded lists
- [ ] Max `limit` enforced on every list endpoint
- [ ] API version in URL path for public APIs
- [ ] Consistent error response format
- [ ] Error responses use `application/problem+json` (RFC 9457)
*Reference: [Google API Design Guide](https://cloud.google.com/apis/design), [Microsoft REST Guidelines](https://github.com/microsoft/api-guidelines)*
@@ -0,0 +1,377 @@
openapi: 3.1.0
info:
title: My API
description: >
Starter spec with 2026-era defaults — RFC 9457 Problem Details,
camelCase JSON, cursor pagination, idempotency keys, rate-limit headers,
and ETag optimistic concurrency. Replace example.com / acme with your own.
version: 1.0.0
contact:
name: API Support
email: support@example.com
license:
name: MIT
identifier: MIT
servers:
- url: http://localhost:3000/v1
description: Local development
- url: https://api.example.com/v1
description: Production
tags:
- name: Users
description: User management
- name: Health
description: Service health checks
security:
- bearerAuth: []
paths:
/health:
get:
tags: [Health]
summary: Health check
operationId: getHealth
security: []
responses:
'200':
description: Service is healthy
content:
application/json:
schema:
type: object
required: [status]
properties:
status: { type: string, example: ok }
timestamp: { type: string, format: date-time }
/users:
get:
tags: [Users]
summary: List users (cursor-paginated)
operationId: listUsers
parameters:
- $ref: '#/components/parameters/CursorParam'
- $ref: '#/components/parameters/LimitParam'
- name: status
in: query
schema: { type: string, enum: [active, inactive] }
responses:
'200':
description: Paginated list of users
headers:
X-RateLimit-Limit: { $ref: '#/components/headers/XRateLimitLimit' }
X-RateLimit-Remaining: { $ref: '#/components/headers/XRateLimitRemaining' }
X-RateLimit-Reset: { $ref: '#/components/headers/XRateLimitReset' }
content:
application/json:
schema: { $ref: '#/components/schemas/UserListResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'429': { $ref: '#/components/responses/TooManyRequests' }
post:
tags: [Users]
summary: Create a user
operationId: createUser
parameters:
- $ref: '#/components/parameters/IdempotencyKeyHeader'
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CreateUserRequest' }
responses:
'201':
description: User created
headers:
Location:
description: URL of the new resource.
schema: { type: string, format: uri }
X-RateLimit-Remaining: { $ref: '#/components/headers/XRateLimitRemaining' }
content:
application/json:
schema: { $ref: '#/components/schemas/User' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'409': { $ref: '#/components/responses/Conflict' }
'422': { $ref: '#/components/responses/ValidationError' }
'429': { $ref: '#/components/responses/TooManyRequests' }
/users/{userId}:
parameters:
- name: userId
in: path
required: true
schema: { type: string, example: usr_abc123 }
get:
tags: [Users]
summary: Get a user by ID
operationId: getUser
responses:
'200':
description: User details
headers:
ETag:
description: Opaque version token. Pass as If-Match when updating.
schema: { type: string }
content:
application/json:
schema: { $ref: '#/components/schemas/User' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
patch:
tags: [Users]
summary: Partially update a user (optimistic concurrency via If-Match)
operationId: updateUser
parameters:
- name: If-Match
in: header
required: true
description: ETag from a prior GET. Prevents lost-update collisions.
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/UpdateUserRequest' }
responses:
'200':
description: User updated
headers:
ETag:
description: New version token after the update.
schema: { type: string }
content:
application/json:
schema: { $ref: '#/components/schemas/User' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'412': { $ref: '#/components/responses/PreconditionFailed' }
'422': { $ref: '#/components/responses/ValidationError' }
delete:
tags: [Users]
summary: Delete a user
operationId: deleteUser
responses:
'204': { description: User deleted }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
apiKeyAuth:
type: apiKey
in: header
name: X-Api-Key
parameters:
CursorParam:
name: cursor
in: query
description: Opaque cursor returned by a previous response.
schema: { type: string }
LimitParam:
name: limit
in: query
description: Maximum items per page.
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
IdempotencyKeyHeader:
name: Idempotency-Key
in: header
required: false
description: >
Client-generated UUID. The server stores the *response* for 24h and
returns the same response for any retry with the same key, so network
errors on unsafe requests can be retried without duplicate side effects.
schema: { type: string, format: uuid }
headers:
XRateLimitLimit:
description: Request quota for the current window.
schema: { type: integer, example: 1000 }
XRateLimitRemaining:
description: Requests remaining in the current window.
schema: { type: integer, example: 942 }
XRateLimitReset:
description: Unix timestamp (seconds) when the quota resets.
schema: { type: integer, example: 1767225600 }
RetryAfterSeconds:
description: Seconds to wait before retrying.
schema: { type: integer, example: 60 }
schemas:
User:
type: object
required: [id, email, name, status, createdAt]
properties:
id: { type: string, example: usr_abc123 }
email: { type: string, format: email, example: jane@example.com }
name: { type: string, example: Jane Doe }
status: { type: string, enum: [active, inactive] }
createdAt: { type: string, format: date-time }
updatedAt: { type: [string, "null"], format: date-time }
CreateUserRequest:
type: object
required: [email, name]
additionalProperties: false
properties:
email: { type: string, format: email, maxLength: 254 }
name: { type: string, minLength: 1, maxLength: 100 }
role:
type: string
enum: [admin, member, viewer]
default: member
UpdateUserRequest:
type: object
additionalProperties: false
properties:
name: { type: string, minLength: 1, maxLength: 100 }
status: { type: string, enum: [active, inactive] }
UserListResponse:
type: object
required: [data, pagination]
properties:
data:
type: array
items: { $ref: '#/components/schemas/User' }
pagination: { $ref: '#/components/schemas/CursorPagination' }
CursorPagination:
type: object
required: [hasMore]
properties:
nextCursor: { type: [string, "null"] }
hasMore: { type: boolean }
ProblemDetails:
type: object
description: RFC 9457 Problem Details for HTTP APIs.
required: [type, title, status]
properties:
type:
type: string
format: uri
description: URI reference identifying the problem type (link to your docs).
example: https://api.example.com/problems/validation-error
title:
type: string
description: Short human-readable summary of the problem class.
example: Validation failed
status:
type: integer
description: HTTP status code.
example: 422
detail:
type: string
description: Human-readable explanation specific to this occurrence.
example: "Field 'email' must be a valid email address."
instance:
type: string
format: uri
description: URI identifying this specific occurrence.
example: /v1/users
errors:
type: array
description: Field-level validation errors (extension member).
items:
type: object
required: [field, message]
properties:
field: { type: string, example: email }
message: { type: string, example: Must be a valid email address. }
code: { type: string, example: invalidFormat }
responses:
BadRequest:
description: Malformed request
content:
application/problem+json:
schema: { $ref: '#/components/schemas/ProblemDetails' }
example:
type: https://api.example.com/problems/bad-request
title: Bad request
status: 400
detail: Request body could not be parsed as JSON.
Unauthorized:
description: Authentication required or invalid
content:
application/problem+json:
schema: { $ref: '#/components/schemas/ProblemDetails' }
example:
type: https://api.example.com/problems/unauthorized
title: Unauthorized
status: 401
detail: Missing or invalid bearer token.
NotFound:
description: Resource not found
content:
application/problem+json:
schema: { $ref: '#/components/schemas/ProblemDetails' }
example:
type: https://api.example.com/problems/not-found
title: Not found
status: 404
detail: User 'usr_abc123' does not exist.
Conflict:
description: Resource conflict (duplicate or state clash)
content:
application/problem+json:
schema: { $ref: '#/components/schemas/ProblemDetails' }
example:
type: https://api.example.com/problems/conflict
title: Conflict
status: 409
detail: A user with that email already exists.
PreconditionFailed:
description: If-Match header did not match current ETag (optimistic concurrency)
content:
application/problem+json:
schema: { $ref: '#/components/schemas/ProblemDetails' }
example:
type: https://api.example.com/problems/precondition-failed
title: Precondition failed
status: 412
detail: The resource was modified since you last fetched it. Re-fetch and retry.
ValidationError:
description: Request validation failed
content:
application/problem+json:
schema: { $ref: '#/components/schemas/ProblemDetails' }
example:
type: https://api.example.com/problems/validation-error
title: Validation failed
status: 422
errors:
- field: email
message: Must be a valid email address.
code: invalidFormat
TooManyRequests:
description: Rate limit exceeded
headers:
Retry-After: { $ref: '#/components/headers/RetryAfterSeconds' }
content:
application/problem+json:
schema: { $ref: '#/components/schemas/ProblemDetails' }
example:
type: https://api.example.com/problems/rate-limited
title: Too many requests
status: 429
detail: Rate limit exceeded. Retry after 60s.
+64
View File
@@ -0,0 +1,64 @@
---
name: owasp
description: >
Use when reviewing code for security vulnerabilities, implementing authentication or authorization flows, handling user input validation, or building web endpoints exposed to untrusted data. Trigger on keywords like XSS, SQL injection, CSRF, input sanitization, password hashing, and security headers. Also apply when auditing existing code for OWASP Top 10 compliance or conducting security-focused code reviews.
---
# OWASP Security Patterns
## When to Use
- Reviewing code for OWASP Top 10 vulnerabilities
- Implementing input validation on user-facing endpoints
- Adding security headers (CSP, HSTS, X-Frame-Options)
- Preventing XSS, SQL injection, CSRF, or SSRF
- Auditing authentication or authorization flows
- Building endpoints that handle untrusted data
## When NOT to Use
- Infrastructure security (network, firewall, cloud IAM) — use platform-specific tools
- Cryptographic algorithm selection — consult cryptography experts
- Compliance frameworks (SOC 2, HIPAA) — security patterns help but don't cover audit requirements
---
## Quick Reference
| Topic | Reference | Key content |
|-------|-----------|-------------|
| All security patterns | `references/patterns.md` | Input validation, SQL injection, XSS, CSRF, auth, headers |
| OWASP Top 10 cheatsheet | `references/owasp-top10-cheatsheet.md` | Quick reference for each vulnerability category |
| Security headers | `references/security-headers.md` | CSP, HSTS, X-Frame-Options, Referrer-Policy |
| Security checklist | `references/security-checklist.md` | Pre-deploy security review checklist |
| Security audit script | `references/security-audit.py` | Automated security scanning utility |
---
## Best Practices
1. **Validate all input at the boundary.** Use Pydantic (Python) or Zod (TypeScript) for schema validation. Never trust client-side validation alone.
2. **Use parameterized queries exclusively.** Never concatenate user input into SQL strings. Use ORM query builders or prepared statements.
3. **Encode output based on context.** HTML-encode for HTML, URL-encode for URLs, JSON-encode for JSON. No single encoding fits all contexts.
4. **Set security headers on every response.** CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy.
5. **Use CSRF tokens for state-changing requests.** Every POST/PUT/DELETE from a browser form needs a CSRF token.
6. **Apply rate limiting to all public endpoints.** Especially authentication, registration, and password reset.
7. **Never expose stack traces or internal errors to clients.** Return generic error messages; log details server-side.
8. **Audit dependencies regularly.** Run `npm audit` / `pip-audit` / `safety check` in CI.
## Common Pitfalls
1. **Relying on client-side validation only** — easily bypassed with curl or browser devtools.
2. **Using `dangerouslySetInnerHTML` or `| safe` without sanitization** — XSS vector.
3. **SQL string concatenation** — even "just for this one query" is a SQL injection risk.
4. **Missing CSRF protection on API routes** — if cookies are used for auth, CSRF applies.
5. **Overly permissive CORS**`Access-Control-Allow-Origin: *` with credentials is a security hole.
6. **Logging sensitive data** — passwords, tokens, and PII in logs persist in storage and backups.
---
## Related Skills
- `authentication` — Secure auth implementation patterns
- `error-handling` — Preventing information leakage through errors
- `backend-frameworks` — Framework-specific security middleware
@@ -1,8 +1,5 @@
---
name: owasp
description: >
Use this skill when reviewing code for security vulnerabilities, implementing authentication or authorization flows, handling user input validation, or building web endpoints exposed to untrusted data. Trigger on keywords like XSS, SQL injection, CSRF, input sanitization, password hashing, and security headers. Also apply when auditing existing code for OWASP Top 10 compliance or conducting security-focused code reviews.
---
# Owasp — Patterns
# OWASP Web Application Security
@@ -550,7 +547,7 @@ Run `npm audit --audit-level=high` and `pip-audit --strict` in CI (e.g., GitHub
## Related Skills
- `patterns/authentication` - Authentication and authorization implementation patterns
- `patterns/error-handling` - Secure error handling that avoids leaking sensitive information
- `devops/docker` — Container security hardening
- `methodology/defense-in-depth` — Multi-layer security validation
- `authentication` - Authentication and authorization implementation patterns
- `error-handling` - Secure error handling that avoids leaking sensitive information
- `docker` — Container security hardening
- `defense-in-depth` — Multi-layer security validation
@@ -1,922 +0,0 @@
---
name: error-handling
description: >
Comprehensive error handling patterns for Python and TypeScript applications. Use this skill whenever writing try/catch blocks, creating custom error classes, implementing retry logic, designing error boundaries in React, building API error responses, or handling failures gracefully. Trigger for any code dealing with exceptions, error propagation, graceful degradation, or fault tolerance.
---
# Error Handling Patterns
## When to Use
- Building API endpoints that must return consistent error responses
- Creating custom exception hierarchies for a domain model
- Implementing retry logic for unreliable network calls or external services
- Designing React error boundaries for component-level fault isolation
- Wrapping third-party libraries that throw unpredictable errors
- Converting between error representations at architectural boundaries (e.g., domain errors to HTTP errors)
- Adopting the Result pattern to avoid exceptions for expected failure paths
## When NOT to Use
- Simple one-off scripts or throwaway prototypes where unhandled crashes are acceptable
- Configuration files, static data, or declarative markup with no runtime logic
- Pure data transformation functions where invalid input should be prevented by types, not caught at runtime
---
## Core Patterns
### 1. Custom Error Classes
Define a hierarchy of domain-specific errors so callers can catch at the right granularity.
**Python**
```python
from enum import Enum
class ErrorCode(str, Enum):
"""Central registry of machine-readable error codes."""
NOT_FOUND = "NOT_FOUND"
VALIDATION_FAILED = "VALIDATION_FAILED"
DUPLICATE_ENTRY = "DUPLICATE_ENTRY"
UNAUTHORIZED = "UNAUTHORIZED"
RATE_LIMITED = "RATE_LIMITED"
EXTERNAL_SERVICE = "EXTERNAL_SERVICE"
INTERNAL = "INTERNAL"
class AppError(Exception):
"""Base error for the entire application.
All domain errors inherit from this so a single except clause
can catch everything the application intentionally raises.
"""
def __init__(
self,
message: str,
code: ErrorCode = ErrorCode.INTERNAL,
*,
details: dict | None = None,
cause: Exception | None = None,
) -> None:
super().__init__(message)
self.code = code
self.details = details or {}
if cause:
self.__cause__ = cause
def to_dict(self) -> dict:
return {
"error": self.code.value,
"message": str(self),
"details": self.details,
}
class NotFoundError(AppError):
def __init__(self, resource: str, identifier: str) -> None:
super().__init__(
f"{resource} with id '{identifier}' not found",
code=ErrorCode.NOT_FOUND,
details={"resource": resource, "id": identifier},
)
class ValidationError(AppError):
def __init__(self, field: str, reason: str) -> None:
super().__init__(
f"Validation failed for '{field}': {reason}",
code=ErrorCode.VALIDATION_FAILED,
details={"field": field, "reason": reason},
)
class ExternalServiceError(AppError):
def __init__(self, service: str, cause: Exception) -> None:
super().__init__(
f"External service '{service}' failed: {cause}",
code=ErrorCode.EXTERNAL_SERVICE,
cause=cause,
details={"service": service},
)
```
**TypeScript**
```typescript
// error-codes.ts
export const ErrorCode = {
NOT_FOUND: "NOT_FOUND",
VALIDATION_FAILED: "VALIDATION_FAILED",
DUPLICATE_ENTRY: "DUPLICATE_ENTRY",
UNAUTHORIZED: "UNAUTHORIZED",
RATE_LIMITED: "RATE_LIMITED",
EXTERNAL_SERVICE: "EXTERNAL_SERVICE",
INTERNAL: "INTERNAL",
} as const;
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
// app-error.ts
export class AppError extends Error {
public readonly code: ErrorCode;
public readonly details: Record<string, unknown>;
constructor(
message: string,
code: ErrorCode = ErrorCode.INTERNAL,
details: Record<string, unknown> = {},
options?: ErrorOptions
) {
super(message, options);
this.name = "AppError";
this.code = code;
this.details = details;
}
toJSON() {
return {
error: this.code,
message: this.message,
details: this.details,
};
}
}
export class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} with id '${id}' not found`, ErrorCode.NOT_FOUND, {
resource,
id,
});
this.name = "NotFoundError";
}
}
export class ValidationError extends AppError {
constructor(field: string, reason: string) {
super(
`Validation failed for '${field}': ${reason}`,
ErrorCode.VALIDATION_FAILED,
{ field, reason }
);
this.name = "ValidationError";
}
}
export class ExternalServiceError extends AppError {
constructor(service: string, cause: Error) {
super(
`External service '${service}' failed: ${cause.message}`,
ErrorCode.EXTERNAL_SERVICE,
{ service },
{ cause }
);
this.name = "ExternalServiceError";
}
}
```
---
### 2. Error Boundaries (React)
Isolate component failures so a single broken widget does not take down the whole page.
**Using react-error-boundary (recommended)**
```typescript
import {
ErrorBoundary,
type FallbackProps,
} from "react-error-boundary";
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert" className="rounded border border-red-300 bg-red-50 p-4">
<h2 className="font-semibold text-red-800">Something went wrong</h2>
<pre className="mt-2 text-sm text-red-700">{error.message}</pre>
<button
onClick={resetErrorBoundary}
className="mt-3 rounded bg-red-600 px-3 py-1 text-white"
>
Try again
</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => {
// Send to error tracking service
reportError({ error, componentStack: info.componentStack });
}}
onReset={() => {
// Clear any stale state before retry
queryClient.clear();
}}
>
<Dashboard />
</ErrorBoundary>
);
}
```
**Granular boundaries per feature**
```typescript
function DashboardPage() {
return (
<div className="grid grid-cols-3 gap-4">
{/* Each widget fails independently */}
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
<RevenueChart />
</ErrorBoundary>
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
<UserActivityFeed />
</ErrorBoundary>
<ErrorBoundary FallbackComponent={WidgetErrorFallback}>
<SystemHealthPanel />
</ErrorBoundary>
</div>
);
}
function WidgetErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div className="flex flex-col items-center justify-center rounded border border-dashed border-gray-300 p-6 text-gray-500">
<p>This widget failed to load.</p>
<button onClick={resetErrorBoundary} className="mt-2 underline">
Retry
</button>
</div>
);
}
```
**Class-based error boundary (when you need full control)**
```typescript
import { Component, type ErrorInfo, type ReactNode } from "react";
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ManualErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error("ErrorBoundary caught:", error, info.componentStack);
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? <p>Something went wrong.</p>;
}
return this.props.children;
}
}
```
---
### 3. Retry Patterns
Retry transient failures with exponential backoff and jitter to avoid thundering herd.
**Python - Retry decorator with exponential backoff**
```python
import asyncio
import random
import logging
from functools import wraps
from typing import TypeVar, Callable, Awaitable
logger = logging.getLogger(__name__)
T = TypeVar("T")
def retry(
max_attempts: int = 3,
base_delay: float = 1.0,
max_delay: float = 30.0,
retryable: tuple[type[Exception], ...] = (Exception,),
) -> Callable:
"""Retry decorator with exponential backoff and full jitter.
Args:
max_attempts: Total number of attempts including the first call.
base_delay: Initial delay in seconds before the first retry.
max_delay: Upper bound on the computed delay.
retryable: Exception types eligible for retry.
"""
def decorator(fn: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
@wraps(fn)
async def wrapper(*args, **kwargs) -> T:
last_exc: Exception | None = None
for attempt in range(1, max_attempts + 1):
try:
return await fn(*args, **kwargs)
except retryable as exc:
last_exc = exc
if attempt == max_attempts:
break
delay = min(base_delay * (2 ** (attempt - 1)), max_delay)
jitter = random.uniform(0, delay)
logger.warning(
"Attempt %d/%d failed (%s), retrying in %.2fs",
attempt,
max_attempts,
exc,
jitter,
)
await asyncio.sleep(jitter)
raise last_exc # type: ignore[misc]
return wrapper
return decorator
# Usage
@retry(max_attempts=3, base_delay=0.5, retryable=(ConnectionError, TimeoutError))
async def fetch_remote_config(url: str) -> dict:
async with httpx.AsyncClient(timeout=5) as client:
resp = await client.get(url)
resp.raise_for_status()
return resp.json()
```
**TypeScript - Retry wrapper**
```typescript
interface RetryOptions {
maxAttempts?: number;
baseDelayMs?: number;
maxDelayMs?: number;
isRetryable?: (error: unknown) => boolean;
}
async function withRetry<T>(
fn: () => Promise<T>,
options: RetryOptions = {}
): Promise<T> {
const {
maxAttempts = 3,
baseDelayMs = 1000,
maxDelayMs = 30_000,
isRetryable = () => true,
} = options;
let lastError: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt === maxAttempts || !isRetryable(error)) {
throw error;
}
const exponential = baseDelayMs * 2 ** (attempt - 1);
const capped = Math.min(exponential, maxDelayMs);
const jitter = Math.random() * capped;
console.warn(
`Attempt ${attempt}/${maxAttempts} failed, retrying in ${jitter.toFixed(0)}ms`
);
await new Promise((resolve) => setTimeout(resolve, jitter));
}
}
throw lastError;
}
// Usage
const data = await withRetry(
() => fetch("/api/config").then((r) => r.json()),
{
maxAttempts: 3,
baseDelayMs: 500,
isRetryable: (err) =>
err instanceof TypeError || (err as Response)?.status >= 500,
}
);
```
---
### 4. Graceful Degradation
When a dependency fails, fall back to a degraded but functional state instead of crashing.
**Circuit breaker pattern (Python)**
```python
import time
from enum import Enum
class CircuitState(Enum):
CLOSED = "closed" # normal operation
OPEN = "open" # failing, reject immediately
HALF_OPEN = "half_open" # testing recovery
class CircuitBreaker:
"""Prevents cascading failures by short-circuiting calls to an unhealthy dependency."""
def __init__(
self,
failure_threshold: int = 5,
recovery_timeout: float = 30.0,
) -> None:
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.state = CircuitState.CLOSED
self.failure_count = 0
self.last_failure_time = 0.0
def _trip(self) -> None:
self.state = CircuitState.OPEN
self.last_failure_time = time.monotonic()
def _reset(self) -> None:
self.state = CircuitState.CLOSED
self.failure_count = 0
async def call(self, fn, *args, fallback=None, **kwargs):
if self.state == CircuitState.OPEN:
if time.monotonic() - self.last_failure_time > self.recovery_timeout:
self.state = CircuitState.HALF_OPEN
else:
if fallback is not None:
return fallback() if callable(fallback) else fallback
raise ExternalServiceError("circuit-breaker", RuntimeError("Circuit open"))
try:
result = await fn(*args, **kwargs)
if self.state == CircuitState.HALF_OPEN:
self._reset()
return result
except Exception as exc:
self.failure_count += 1
if self.failure_count >= self.failure_threshold:
self._trip()
raise
# Usage
recommendations_circuit = CircuitBreaker(failure_threshold=3, recovery_timeout=60)
async def get_recommendations(user_id: str) -> list[dict]:
return await recommendations_circuit.call(
recommendation_service.fetch,
user_id,
fallback=lambda: [], # empty list when service is down
)
```
**Feature-flag degraded mode (TypeScript)**
```typescript
interface FeatureFlags {
enableRecommendations: boolean;
enableRealTimeUpdates: boolean;
enableAdvancedSearch: boolean;
}
const defaultFlags: FeatureFlags = {
enableRecommendations: true,
enableRealTimeUpdates: true,
enableAdvancedSearch: true,
};
async function getFlags(): Promise<FeatureFlags> {
try {
const resp = await fetch("/api/feature-flags");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return await resp.json();
} catch {
// Fall back to safe defaults when flag service is unavailable
console.warn("Feature flag service unavailable, using defaults");
return defaultFlags;
}
}
// Component that degrades gracefully
function SearchPage() {
const flags = useFeatureFlags();
return (
<div>
<BasicSearch />
{flags.enableAdvancedSearch ? (
<AdvancedFilters />
) : (
<p className="text-sm text-gray-500">
Advanced search is temporarily unavailable.
</p>
)}
</div>
);
}
```
---
### 5. API Error Responses
Return consistent, machine-readable error payloads following RFC 7807 Problem Details.
**Python (FastAPI)**
```python
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from starlette.status import (
HTTP_400_BAD_REQUEST,
HTTP_404_NOT_FOUND,
HTTP_429_TOO_MANY_REQUESTS,
HTTP_500_INTERNAL_SERVER_ERROR,
)
app = FastAPI()
# Map domain error codes to HTTP status codes
STATUS_MAP: dict[ErrorCode, int] = {
ErrorCode.NOT_FOUND: HTTP_404_NOT_FOUND,
ErrorCode.VALIDATION_FAILED: HTTP_400_BAD_REQUEST,
ErrorCode.DUPLICATE_ENTRY: 409,
ErrorCode.UNAUTHORIZED: 401,
ErrorCode.RATE_LIMITED: HTTP_429_TOO_MANY_REQUESTS,
ErrorCode.EXTERNAL_SERVICE: 502,
ErrorCode.INTERNAL: HTTP_500_INTERNAL_SERVER_ERROR,
}
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError) -> JSONResponse:
status = STATUS_MAP.get(exc.code, 500)
return JSONResponse(
status_code=status,
content={
"type": f"https://docs.example.com/errors/{exc.code.value.lower()}",
"title": exc.code.value.replace("_", " ").title(),
"status": status,
"detail": str(exc),
**exc.details,
},
)
@app.exception_handler(Exception)
async def unhandled_error_handler(request: Request, exc: Exception) -> JSONResponse:
# Never leak internal details to the client
logger.exception("Unhandled exception on %s %s", request.method, request.url.path)
return JSONResponse(
status_code=500,
content={
"type": "https://docs.example.com/errors/internal",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred.",
},
)
```
**TypeScript (Express middleware)**
```typescript
import type { Request, Response, NextFunction } from "express";
const STATUS_MAP: Record<ErrorCode, number> = {
NOT_FOUND: 404,
VALIDATION_FAILED: 400,
DUPLICATE_ENTRY: 409,
UNAUTHORIZED: 401,
RATE_LIMITED: 429,
EXTERNAL_SERVICE: 502,
INTERNAL: 500,
};
function errorHandler(
err: Error,
_req: Request,
res: Response,
_next: NextFunction
) {
if (err instanceof AppError) {
const status = STATUS_MAP[err.code] ?? 500;
res.status(status).json({
type: `https://docs.example.com/errors/${err.code.toLowerCase()}`,
title: err.code.replace(/_/g, " ").toLowerCase(),
status,
detail: err.message,
...err.details,
});
return;
}
// Unhandled errors - log full details, return generic message
console.error("Unhandled error:", err);
res.status(500).json({
type: "https://docs.example.com/errors/internal",
title: "Internal Server Error",
status: 500,
detail: "An unexpected error occurred.",
});
}
// Register as the last middleware
app.use(errorHandler);
```
---
### 6. Error Logging Integration
Attach structured context to errors so they are searchable and actionable in observability tools.
**Python - Structured error logging**
```python
import logging
import traceback
from contextvars import ContextVar
request_id_var: ContextVar[str] = ContextVar("request_id", default="unknown")
class StructuredErrorLogger:
"""Wraps the standard logger to attach error context automatically."""
def __init__(self, name: str) -> None:
self.logger = logging.getLogger(name)
def error(
self,
msg: str,
*,
exc: Exception | None = None,
extra: dict | None = None,
) -> None:
context = {
"request_id": request_id_var.get(),
**(extra or {}),
}
if exc is not None:
context["error_type"] = type(exc).__name__
context["error_message"] = str(exc)
context["stacktrace"] = traceback.format_exception(exc)
if isinstance(exc, AppError):
context["error_code"] = exc.code.value
context["error_details"] = exc.details
self.logger.error(msg, extra={"structured": context}, exc_info=exc)
# Usage in a FastAPI middleware
from starlette.middleware.base import BaseHTTPMiddleware
import uuid
log = StructuredErrorLogger(__name__)
class ErrorLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
rid = request.headers.get("x-request-id", str(uuid.uuid4()))
request_id_var.set(rid)
try:
response = await call_next(request)
return response
except Exception as exc:
log.error(
"Request failed",
exc=exc,
extra={"method": request.method, "path": request.url.path},
)
raise
```
**TypeScript - Error context enrichment**
```typescript
interface ErrorContext {
requestId?: string;
userId?: string;
operation?: string;
[key: string]: unknown;
}
function logError(
message: string,
error: unknown,
context: ErrorContext = {}
): void {
const payload: Record<string, unknown> = {
message,
timestamp: new Date().toISOString(),
...context,
};
if (error instanceof AppError) {
payload.errorCode = error.code;
payload.errorMessage = error.message;
payload.errorDetails = error.details;
} else if (error instanceof Error) {
payload.errorType = error.name;
payload.errorMessage = error.message;
payload.stack = error.stack;
} else {
payload.errorRaw = String(error);
}
// Structured JSON log for ingestion by Datadog, CloudWatch, etc.
console.error(JSON.stringify(payload));
}
// Usage
try {
await processOrder(orderId);
} catch (error) {
logError("Order processing failed", error, {
requestId: req.headers["x-request-id"],
operation: "processOrder",
orderId,
});
throw error;
}
```
---
### 7. Result Pattern
Use a Result type for operations where failure is an expected outcome. Avoids exception overhead and makes the failure path explicit in the type signature.
**Python - Using the `result` library**
```python
# pip install result
from result import Ok, Err, Result
def parse_age(value: str) -> Result[int, str]:
"""Parse a string to a valid age. Returns Err for invalid input."""
try:
age = int(value)
except ValueError:
return Err(f"'{value}' is not a number")
if age < 0 or age > 150:
return Err(f"Age {age} is out of valid range (0-150)")
return Ok(age)
def validate_registration(data: dict) -> Result[dict, list[str]]:
"""Validate all fields, collecting every error instead of failing on the first."""
errors: list[str] = []
match parse_age(data.get("age", "")):
case Ok(age):
data["age"] = age
case Err(msg):
errors.append(msg)
name = data.get("name", "").strip()
if not name:
errors.append("Name is required")
if len(name) > 100:
errors.append("Name must be 100 characters or fewer")
if errors:
return Err(errors)
return Ok(data)
# Caller handles both paths explicitly
match validate_registration(form_data):
case Ok(valid):
user = create_user(valid)
case Err(errs):
return {"errors": errs}, 400
```
**TypeScript - Discriminated union Result**
```typescript
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
// Usage
function parseAge(input: string): Result<number, string> {
const age = Number(input);
if (Number.isNaN(age)) return err(`'${input}' is not a number`);
if (age < 0 || age > 150) return err(`Age ${age} out of range (0-150)`);
return ok(age);
}
function validateRegistration(
data: Record<string, string>
): Result<{ name: string; age: number }, string[]> {
const errors: string[] = [];
const ageResult = parseAge(data.age ?? "");
if (!ageResult.ok) errors.push(ageResult.error);
const name = data.name?.trim() ?? "";
if (!name) errors.push("Name is required");
if (name.length > 100) errors.push("Name must be 100 characters or fewer");
if (errors.length > 0) return err(errors);
return ok({ name, age: (ageResult as { ok: true; value: number }).value });
}
// Caller
const result = validateRegistration(formData);
if (!result.ok) {
return res.status(400).json({ errors: result.error });
}
const user = await createUser(result.value);
```
---
## Best Practices
1. **Catch specific exceptions, not bare `except` or `catch`.** A catch-all hides bugs. Catch only the errors you know how to handle and let everything else propagate.
2. **Translate errors at architectural boundaries.** A database `IntegrityError` should become a domain `DuplicateEntryError` at the repository layer, then an HTTP 409 at the API layer. Each layer speaks its own error language.
3. **Preserve the original cause.** Always chain the original exception (`raise X from original` in Python, `{ cause }` in TypeScript) so the root cause is visible in logs and debuggers.
4. **Fail fast, recover high.** Detect errors as early as possible (validate inputs at the boundary) but handle them at the highest level that has enough context to decide what to do (e.g., return an HTTP response, show a fallback UI).
5. **Never swallow errors silently.** An empty `except: pass` or `catch {}` is almost always a bug. At minimum, log the error. If you intentionally ignore it, leave a comment explaining why.
6. **Use the Result pattern for expected failures.** When a function can legitimately fail (parsing, validation, lookups), return a Result instead of throwing. Reserve exceptions for truly unexpected situations.
7. **Make errors actionable.** Every error message should help the reader fix the problem. Include what happened, what was expected, and what the caller can do about it. `"User not found"` is worse than `"User with id '123' not found. Verify the id and check that the user has not been deleted."`.
8. **Test the error paths.** Write explicit tests for every error branch. Verify the error type, message, and status code. Error paths that are never tested are error paths that will break in production.
---
## Common Pitfalls
1. **Catching too broadly.** Using `except Exception` or `catch (e: any)` silences programming errors like `TypeError` or `ReferenceError` that should crash loudly during development.
2. **Logging and re-throwing without deduplication.** If every layer logs the same error, you get five log entries for one failure. Log at the outermost handler and let inner layers propagate.
3. **Returning error data in the wrong shape.** Mixing `{ error: "..." }`, `{ message: "..." }`, and `{ errors: [...] }` across endpoints forces every client to handle multiple formats. Pick one shape and enforce it globally.
4. **Leaking internal details to clients.** Stack traces, database table names, and file paths in API responses are a security risk. Sanitize errors before they leave the server.
5. **Retrying non-idempotent operations.** Retrying a `POST /orders` that partially succeeded can create duplicate orders. Only retry operations that are safe to repeat, or use idempotency keys.
6. **Ignoring async error boundaries.** In React, error boundaries do not catch errors inside event handlers or async callbacks. Use try/catch inside `onClick`, `useEffect` cleanup, and promise chains separately.
---
## Related Skills
- `patterns/logging` - Structured logging setup and conventions
- `patterns/api-client` - HTTP client wrappers with built-in error handling
- `security/owasp` - Preventing information leakage through error messages
- `languages/python` - Python exception syntax and idioms
- `languages/typescript` - TypeScript error types and narrowing
-962
View File
@@ -1,962 +0,0 @@
---
name: logging
description: >
Structured logging patterns for Python and Node.js applications. Use this skill when setting up loggers, choosing log levels, implementing correlation IDs for request tracing, redacting sensitive data from logs, or configuring log aggregation. Trigger whenever code uses console.log, print(), logging module, winston, pino, structlog, or any logging library. Also applies when building observability, debugging production issues, or adding telemetry.
---
# Logging
## When to Use
- Setting up structured logging in a new application or service
- Replacing `console.log` or `print()` with proper logging infrastructure
- Adding request tracing with correlation IDs across microservices
- Redacting sensitive data (passwords, tokens, PII) from log output
- Building observability pipelines with log aggregation (ELK, Datadog, CloudWatch)
## When NOT to Use
- Static analysis or linting tasks that do not involve runtime output
- Pure computation functions where logging would add unnecessary noise
- Test assertions — use testing frameworks' built-in assertion messages, not log output
---
## Core Patterns
### 1. Structured Logging Setup
Structured logging outputs machine-parseable JSON instead of free-form strings. This enables searching, filtering, and alerting in log aggregation systems.
#### Python with structlog
```python
# logging_config.py
import logging
import structlog
def configure_logging(log_level: str = "INFO", json_output: bool = True) -> None:
"""Configure structured logging for the application.
Call this once at application startup, before any loggers are created.
"""
# Set the stdlib logging level as the baseline filter
logging.basicConfig(
format="%(message)s",
level=getattr(logging, log_level.upper()),
)
# Choose renderers based on environment
if json_output:
renderer = structlog.processors.JSONRenderer()
else:
# Human-readable output for local development
renderer = structlog.dev.ConsoleRenderer(colors=True)
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
renderer,
],
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
```
```python
# Usage anywhere in the application
import structlog
logger = structlog.get_logger(__name__)
async def create_user(email: str) -> User:
logger.info("creating_user", email=email)
user = await user_repo.create(email=email)
logger.info("user_created", user_id=user.id, email=email)
return user
```
**Output (JSON mode):**
```json
{"event": "user_created", "user_id": 42, "email": "alice@example.com", "logger": "app.services.user", "level": "info", "timestamp": "2025-06-15T10:30:00.123Z"}
```
#### Node.js with pino
```typescript
// logger.ts
import pino from "pino";
export const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
// Use pretty printing only in development
transport:
process.env.NODE_ENV === "development"
? { target: "pino-pretty", options: { colorize: true } }
: undefined,
// Base fields included in every log line
base: {
service: process.env.SERVICE_NAME ?? "api",
version: process.env.APP_VERSION ?? "unknown",
},
// Customize serialization
serializers: {
err: pino.stdSerializers.err,
req: pino.stdSerializers.req,
res: pino.stdSerializers.res,
},
// Redact sensitive fields (see Pattern 4)
redact: ["req.headers.authorization", "req.headers.cookie"],
});
// Create child loggers for specific modules
export function createLogger(module: string): pino.Logger {
return logger.child({ module });
}
```
```typescript
// Usage in a service
import { createLogger } from "./logger";
const log = createLogger("user-service");
export async function createUser(email: string): Promise<User> {
log.info({ email }, "creating_user");
const user = await userRepo.create({ email });
log.info({ userId: user.id, email }, "user_created");
return user;
}
```
**Output (JSON):**
```json
{"level":30,"time":1718444400123,"service":"api","module":"user-service","userId":42,"email":"alice@example.com","msg":"user_created"}
```
### 2. Log Levels
Use log levels consistently to control verbosity and enable filtering in production.
| Level | When to Use | Example |
|-------|-------------|---------|
| `DEBUG` | Detailed diagnostic information useful only during development or debugging | Variable values, SQL queries, cache hits/misses |
| `INFO` | Confirmation that things are working as expected | Request received, user created, job completed |
| `WARNING` | Something unexpected happened but the application can continue | Deprecated API called, retry attempt, approaching rate limit |
| `ERROR` | A specific operation failed but the application continues running | Database query failed, external API returned 500, payment declined |
| `CRITICAL` | The application cannot continue or is in an unrecoverable state | Database connection pool exhausted, out of disk space, configuration missing |
#### Python examples
```python
import structlog
logger = structlog.get_logger(__name__)
# DEBUG: Detailed internals for troubleshooting
logger.debug("cache_lookup", key="user:42", hit=True, ttl_remaining=120)
# INFO: Normal business events
logger.info("order_placed", order_id="ORD-123", total=99.99, items=3)
# WARNING: Degraded but functional
logger.warning(
"rate_limit_approaching",
current_rate=450,
limit=500,
window_seconds=60,
)
# ERROR: Operation failed, needs attention
logger.error(
"payment_failed",
order_id="ORD-123",
provider="stripe",
error_code="card_declined",
exc_info=True, # Include stack trace
)
# CRITICAL: System-level failure
logger.critical(
"database_pool_exhausted",
active_connections=100,
max_connections=100,
waiting_requests=47,
)
```
#### TypeScript examples
```typescript
import { createLogger } from "./logger";
const log = createLogger("order-service");
// DEBUG: Internal details
log.debug({ key: "user:42", hit: true, ttlRemaining: 120 }, "cache_lookup");
// INFO: Normal events
log.info({ orderId: "ORD-123", total: 99.99, items: 3 }, "order_placed");
// WARNING: Degraded state
log.warn(
{ currentRate: 450, limit: 500, windowSeconds: 60 },
"rate_limit_approaching"
);
// ERROR: Operation failure
log.error(
{ orderId: "ORD-123", provider: "stripe", errorCode: "card_declined" },
"payment_failed"
);
// FATAL: Unrecoverable
log.fatal(
{ activeConnections: 100, maxConnections: 100, waitingRequests: 47 },
"database_pool_exhausted"
);
```
**Level selection rule of thumb:** If you would page someone at 3 AM, it is ERROR or CRITICAL. If it is useful context for investigating an issue, it is INFO. If it is only useful when actively debugging a specific problem, it is DEBUG.
### 3. Correlation IDs
Correlation IDs (also called request IDs or trace IDs) tie together all log entries from a single request as it flows through your system.
#### Python — FastAPI middleware with contextvars
```python
# middleware/correlation.py
import uuid
from contextvars import ContextVar
import structlog
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
# Context variable accessible from any async task in the same request
correlation_id_var: ContextVar[str] = ContextVar("correlation_id", default="")
class CorrelationIDMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Accept an incoming correlation ID or generate a new one
correlation_id = request.headers.get("X-Correlation-ID", uuid.uuid4().hex)
correlation_id_var.set(correlation_id)
# Bind to structlog context so all logs in this request include it
structlog.contextvars.clear_contextvars()
structlog.contextvars.bind_contextvars(correlation_id=correlation_id)
response = await call_next(request)
response.headers["X-Correlation-ID"] = correlation_id
return response
```
```python
# Register the middleware
from middleware.correlation import CorrelationIDMiddleware
app.add_middleware(CorrelationIDMiddleware)
```
```python
# Any logger call in any module now includes correlation_id automatically
logger = structlog.get_logger(__name__)
async def get_user(user_id: int) -> User:
logger.info("fetching_user", user_id=user_id)
# Output: {"event": "fetching_user", "user_id": 42, "correlation_id": "a1b2c3d4...", ...}
return await user_repo.get(user_id)
```
#### TypeScript — Express middleware with AsyncLocalStorage
```typescript
// middleware/correlation.ts
import { AsyncLocalStorage } from "node:async_hooks";
import { randomUUID } from "node:crypto";
import type { Request, Response, NextFunction } from "express";
import { logger } from "../logger";
interface RequestContext {
correlationId: string;
}
export const asyncLocalStorage = new AsyncLocalStorage<RequestContext>();
export function correlationMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
const correlationId =
(req.headers["x-correlation-id"] as string) ?? randomUUID();
res.setHeader("X-Correlation-ID", correlationId);
asyncLocalStorage.run({ correlationId }, () => {
next();
});
}
```
```typescript
// logger.ts — augment the logger to include correlation ID
import { asyncLocalStorage } from "./middleware/correlation";
export function getContextLogger(): pino.Logger {
const store = asyncLocalStorage.getStore();
if (store) {
return logger.child({ correlationId: store.correlationId });
}
return logger;
}
```
```typescript
// Usage in any module
import { getContextLogger } from "./logger";
export async function getUser(userId: number): Promise<User> {
const log = getContextLogger();
log.info({ userId }, "fetching_user");
// Output includes correlationId automatically
return await userRepo.findById(userId);
}
```
#### Propagating to downstream services
When calling other microservices, forward the correlation ID:
```python
# Python — httpx client
import httpx
from middleware.correlation import correlation_id_var
async def call_billing_service(user_id: int) -> dict:
correlation_id = correlation_id_var.get()
async with httpx.AsyncClient() as client:
response = await client.get(
f"http://billing-service/api/v1/invoices?user_id={user_id}",
headers={"X-Correlation-ID": correlation_id},
)
return response.json()
```
```typescript
// TypeScript — fetch with correlation ID
import { asyncLocalStorage } from "./middleware/correlation";
export async function callBillingService(userId: number): Promise<Invoice[]> {
const store = asyncLocalStorage.getStore();
const response = await fetch(
`http://billing-service/api/v1/invoices?user_id=${userId}`,
{
headers: {
"X-Correlation-ID": store?.correlationId ?? "",
},
}
);
return response.json();
}
```
### 4. Sensitive Data Redaction
Never log passwords, API keys, tokens, credit card numbers, or personally identifiable information (PII). Build redaction into the logging pipeline so developers cannot accidentally leak secrets.
#### Python — structlog processor
```python
# processors/redact.py
import re
from typing import Any
# Patterns for sensitive field names (case-insensitive matching)
SENSITIVE_KEYS = re.compile(
r"(password|passwd|secret|token|api_key|apikey|authorization|"
r"credit_card|card_number|cvv|ssn|social_security)",
re.IGNORECASE,
)
# Pattern for credit card numbers in string values
CREDIT_CARD_PATTERN = re.compile(r"\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b")
# Pattern for bearer tokens in string values
BEARER_PATTERN = re.compile(r"Bearer\s+[A-Za-z0-9\-._~+/]+=*", re.IGNORECASE)
def redact_sensitive_data(
logger: Any, method_name: str, event_dict: dict
) -> dict:
"""Structlog processor that masks sensitive values."""
return _redact_dict(event_dict)
def _redact_dict(data: dict) -> dict:
result = {}
for key, value in data.items():
if SENSITIVE_KEYS.search(key):
result[key] = "***REDACTED***"
elif isinstance(value, dict):
result[key] = _redact_dict(value)
elif isinstance(value, str):
result[key] = _redact_string(value)
elif isinstance(value, list):
result[key] = [
_redact_dict(item) if isinstance(item, dict) else item
for item in value
]
else:
result[key] = value
return result
def _redact_string(value: str) -> str:
value = CREDIT_CARD_PATTERN.sub("****-****-****-****", value)
value = BEARER_PATTERN.sub("Bearer ***REDACTED***", value)
return value
```
```python
# Add the processor to structlog configuration
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
redact_sensitive_data, # Add before the renderer
structlog.processors.JSONRenderer(),
],
# ...
)
```
#### TypeScript — pino redaction
```typescript
// pino has built-in redaction support
import pino from "pino";
export const logger = pino({
level: "info",
redact: {
paths: [
"password",
"secret",
"token",
"apiKey",
"authorization",
"creditCard",
"req.headers.authorization",
"req.headers.cookie",
"body.password",
"body.creditCardNumber",
"*.password",
"*.secret",
],
censor: "***REDACTED***",
},
});
```
For more complex redaction (regex-based), use a custom serializer:
```typescript
// redact.ts
const CREDIT_CARD_RE = /\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g;
const BEARER_RE = /Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi;
export function redactValue(value: unknown): unknown {
if (typeof value === "string") {
let result = value.replace(CREDIT_CARD_RE, "****-****-****-****");
result = result.replace(BEARER_RE, "Bearer ***REDACTED***");
return result;
}
if (typeof value === "object" && value !== null) {
return redactObject(value as Record<string, unknown>);
}
return value;
}
const SENSITIVE_KEYS =
/^(password|passwd|secret|token|api_?key|authorization|credit_?card|cvv|ssn)$/i;
function redactObject(obj: Record<string, unknown>): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
if (SENSITIVE_KEYS.test(key)) {
result[key] = "***REDACTED***";
} else {
result[key] = redactValue(value);
}
}
return result;
}
```
### 5. Request/Response Logging
Log every HTTP request and response with method, path, status code, duration, and body size. This is the single most valuable log line for production debugging.
#### Python — FastAPI middleware
```python
# middleware/request_logging.py
import time
import structlog
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
logger = structlog.get_logger("http")
class RequestLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start_time = time.perf_counter()
# Log request
logger.info(
"http_request_started",
method=request.method,
path=request.url.path,
query=str(request.url.query) or None,
client_ip=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent"),
)
try:
response = await call_next(request)
except Exception:
duration_ms = (time.perf_counter() - start_time) * 1000
logger.error(
"http_request_failed",
method=request.method,
path=request.url.path,
duration_ms=round(duration_ms, 2),
exc_info=True,
)
raise
duration_ms = (time.perf_counter() - start_time) * 1000
content_length = response.headers.get("content-length")
# Choose log level based on status code
log_method = logger.info
if response.status_code >= 500:
log_method = logger.error
elif response.status_code >= 400:
log_method = logger.warning
log_method(
"http_request_completed",
method=request.method,
path=request.url.path,
status_code=response.status_code,
duration_ms=round(duration_ms, 2),
content_length=int(content_length) if content_length else None,
)
return response
```
#### TypeScript — Express middleware
```typescript
// middleware/request-logging.ts
import type { Request, Response, NextFunction } from "express";
import { getContextLogger } from "../logger";
export function requestLogging(
req: Request,
res: Response,
next: NextFunction
): void {
const start = process.hrtime.bigint();
const log = getContextLogger();
log.info(
{
method: req.method,
path: req.originalUrl,
clientIp: req.ip,
userAgent: req.get("user-agent"),
},
"http_request_started"
);
// Hook into the response finish event
res.on("finish", () => {
const durationMs =
Number(process.hrtime.bigint() - start) / 1_000_000;
const logFn =
res.statusCode >= 500
? log.error.bind(log)
: res.statusCode >= 400
? log.warn.bind(log)
: log.info.bind(log);
logFn(
{
method: req.method,
path: req.originalUrl,
statusCode: res.statusCode,
durationMs: Math.round(durationMs * 100) / 100,
contentLength: res.get("content-length")
? parseInt(res.get("content-length")!, 10)
: undefined,
},
"http_request_completed"
);
});
next();
}
```
```typescript
// Register middleware (order matters)
app.use(correlationMiddleware);
app.use(requestLogging);
```
### 6. Error Logging
When logging errors, always include the stack trace, relevant context, and enough information to reproduce the issue without accessing the production environment.
#### Python — exception logging with context
```python
import structlog
logger = structlog.get_logger(__name__)
async def process_order(order_id: str) -> Order:
logger.info("processing_order", order_id=order_id)
try:
order = await order_repo.get(order_id)
if not order:
logger.error("order_not_found", order_id=order_id)
raise OrderNotFoundError(order_id)
payment = await payment_service.charge(
amount=order.total,
currency=order.currency,
customer_id=order.customer_id,
)
logger.info(
"payment_processed",
order_id=order_id,
payment_id=payment.id,
amount=order.total,
)
except PaymentError as exc:
# Log the error with full context for debugging
logger.error(
"payment_failed",
order_id=order_id,
customer_id=order.customer_id,
amount=order.total,
error_code=exc.code,
error_message=str(exc),
exc_info=True, # Includes full stack trace
)
raise
except Exception as exc:
# Catch-all for unexpected errors
logger.exception(
"order_processing_unexpected_error",
order_id=order_id,
error_type=type(exc).__name__,
)
raise
```
#### TypeScript — error logging with breadcrumbs
```typescript
import { getContextLogger } from "./logger";
const log = getContextLogger();
export async function processOrder(orderId: string): Promise<Order> {
log.info({ orderId }, "processing_order");
try {
const order = await orderRepo.findById(orderId);
if (!order) {
log.error({ orderId }, "order_not_found");
throw new OrderNotFoundError(orderId);
}
const payment = await paymentService.charge({
amount: order.total,
currency: order.currency,
customerId: order.customerId,
});
log.info(
{ orderId, paymentId: payment.id, amount: order.total },
"payment_processed"
);
return order;
} catch (err) {
if (err instanceof PaymentError) {
log.error(
{
orderId,
errorCode: err.code,
errorMessage: err.message,
err, // pino serializes Error objects with stack traces
},
"payment_failed"
);
} else {
log.error(
{
orderId,
err,
errorType: (err as Error).constructor.name,
},
"order_processing_unexpected_error"
);
}
throw err;
}
}
```
**Key principle:** Log at the point where you have the most context, not at every layer. A single error log with full context is more useful than five partial logs scattered across the call stack.
### 7. Performance Logging
Track operation durations to identify slow endpoints, queries, and external calls.
#### Python — timing decorator
```python
import functools
import time
from typing import Callable, TypeVar
import structlog
logger = structlog.get_logger("performance")
F = TypeVar("F", bound=Callable)
def log_duration(operation: str, slow_threshold_ms: float = 1000.0):
"""Decorator that logs the duration of a function call.
Args:
operation: A descriptive name for the operation.
slow_threshold_ms: Threshold in milliseconds above which
the log level escalates to WARNING.
"""
def decorator(func: F) -> F:
@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
start = time.perf_counter()
try:
result = await func(*args, **kwargs)
return result
finally:
duration_ms = (time.perf_counter() - start) * 1000
log_fn = (
logger.warning
if duration_ms > slow_threshold_ms
else logger.debug
)
log_fn(
"operation_duration",
operation=operation,
duration_ms=round(duration_ms, 2),
slow=duration_ms > slow_threshold_ms,
)
@functools.wraps(func)
def sync_wrapper(*args, **kwargs):
start = time.perf_counter()
try:
result = func(*args, **kwargs)
return result
finally:
duration_ms = (time.perf_counter() - start) * 1000
log_fn = (
logger.warning
if duration_ms > slow_threshold_ms
else logger.debug
)
log_fn(
"operation_duration",
operation=operation,
duration_ms=round(duration_ms, 2),
slow=duration_ms > slow_threshold_ms,
)
import asyncio
if asyncio.iscoroutinefunction(func):
return async_wrapper # type: ignore
return sync_wrapper # type: ignore
return decorator
# Usage
@log_duration("fetch_user_profile", slow_threshold_ms=200)
async def get_user_profile(user_id: int) -> UserProfile:
return await user_repo.get_with_preferences(user_id)
```
#### Python — context manager for ad-hoc timing
```python
import time
from contextlib import contextmanager
import structlog
logger = structlog.get_logger("performance")
@contextmanager
def log_timing(operation: str, **extra_fields):
"""Context manager for timing arbitrary code blocks."""
start = time.perf_counter()
yield
duration_ms = (time.perf_counter() - start) * 1000
logger.info(
"operation_duration",
operation=operation,
duration_ms=round(duration_ms, 2),
**extra_fields,
)
# Usage
async def rebuild_search_index():
with log_timing("rebuild_search_index", index="products"):
products = await product_repo.get_all()
await search_service.reindex(products)
```
#### TypeScript — timing wrapper
```typescript
import { createLogger } from "./logger";
const perfLog = createLogger("performance");
export async function withTiming<T>(
operation: string,
fn: () => Promise<T>,
slowThresholdMs = 1000
): Promise<T> {
const start = performance.now();
try {
const result = await fn();
return result;
} finally {
const durationMs = performance.now() - start;
const logFn = durationMs > slowThresholdMs ? perfLog.warn : perfLog.debug;
logFn(
{
operation,
durationMs: Math.round(durationMs * 100) / 100,
slow: durationMs > slowThresholdMs,
},
"operation_duration"
);
}
}
// Usage
const profile = await withTiming("fetch_user_profile", () =>
userRepo.getWithPreferences(userId)
);
```
#### Slow query logging
```python
# Python — SQLAlchemy event listener for slow queries
from sqlalchemy import event
from sqlalchemy.engine import Engine
import structlog
logger = structlog.get_logger("database")
SLOW_QUERY_THRESHOLD_MS = 500
@event.listens_for(Engine, "before_cursor_execute")
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
conn.info.setdefault("query_start_time", []).append(time.perf_counter())
@event.listens_for(Engine, "after_cursor_execute")
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
total_ms = (time.perf_counter() - conn.info["query_start_time"].pop()) * 1000
if total_ms > SLOW_QUERY_THRESHOLD_MS:
logger.warning(
"slow_query",
duration_ms=round(total_ms, 2),
statement=statement[:500], # Truncate long queries
parameters=str(parameters)[:200],
)
```
---
## Best Practices
1. **Use structured logging from day one** — start with JSON output and key-value pairs instead of formatted strings. Switching from `f"User {user_id} created"` to `logger.info("user_created", user_id=user_id)` costs nothing upfront but saves hours when debugging in production.
2. **Log events, not sentences** — use snake_case event names (`order_placed`, `payment_failed`) rather than prose messages (`"An order was placed by the user"`). Event names are searchable, filterable, and easy to aggregate.
3. **Include the right context at the right level** — every log line should include enough context to be useful in isolation: relevant IDs (user, order, request), operation name, and outcome. Avoid logging the same error at every layer of the call stack.
4. **Set log levels per environment** — use DEBUG in development, INFO in staging, and INFO or WARNING in production. Never leave DEBUG enabled in production — it generates excessive volume and may expose sensitive internals.
5. **Centralize logging configuration** — configure loggers once at application startup, not in individual modules. Every module should call `get_logger(__name__)` and inherit the shared configuration.
6. **Always redact sensitive data** — build redaction into the logging pipeline as a processor or serializer. Do not rely on developers remembering to exclude passwords or tokens from log calls.
7. **Use correlation IDs for every request** — generate a unique ID at the entry point and propagate it through all downstream calls. This is the single most important pattern for debugging distributed systems.
8. **Set up log rotation and retention policies** — configure maximum file sizes, rotation intervals, and retention periods. Production logs without rotation will fill disks. Use log aggregation services (ELK, Datadog, CloudWatch) rather than relying on local files.
---
## Common Pitfalls
1. **Logging sensitive data** — passwords, API keys, JWTs, credit card numbers, and PII end up in logs more often than expected. Once written, they persist in log storage and backups. Build redaction into the pipeline rather than relying on code review to catch every instance.
2. **Using print() or console.log in production**`print()` in Python and `console.log` in Node.js write to stdout without timestamps, levels, or structure. They cannot be filtered, aggregated, or searched. Replace them with a proper logger before deploying.
3. **Logging too much at high levels** — setting every log call to INFO or ERROR creates alert fatigue and obscures real problems. Use DEBUG for diagnostic details and reserve ERROR for situations that require action.
4. **Missing stack traces on errors** — logging `str(exception)` loses the stack trace. In Python, use `exc_info=True` or `logger.exception()`. In pino, pass the error as `{ err }` to get the full stack serialized.
5. **Not testing log output** — logging code is code. If your redaction processor has a bug, secrets leak. Write unit tests that capture log output and assert on structure, redacted fields, and expected context.
6. **Synchronous logging in async applications** — writing to files or network sinks synchronously from an async event loop blocks request processing. Use async-compatible transports (pino's worker thread, structlog with stdlib async handlers) or write to stdout and let the infrastructure handle routing.
---
## Related Skills
- `patterns/error-handling` — Exception handling patterns that complement error logging
- `patterns/api-client` — HTTP client patterns including logging outbound requests
- `frameworks/fastapi` — FastAPI middleware setup for request logging and correlation IDs
- `devops/docker` — Container logging drivers and log aggregation in Docker environments
- `databases/postgresql` — Logging database queries and slow query detection
- `databases/mongodb` — Logging database operations and aggregation pipelines

Some files were not shown because too many files have changed in this diff Show More