mirror of
https://github.com/duthaho/claudekit.git
synced 2026-06-10 12:14:57 +03:00
feat: enhanced the writing skills
This commit is contained in:
+18
-17
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
+7
-10
@@ -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
|
||||
@@ -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 }]
|
||||
@@ -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
|
||||
+6
-9
@@ -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
|
||||
+6
-9
@@ -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
|
||||
+9
-12
@@ -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
|
||||
@@ -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
|
||||
+61
-3
@@ -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
|
||||
@@ -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
|
||||
+7
-10
@@ -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
|
||||
@@ -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
|
||||
+5
-8
@@ -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
|
||||
+5
-8
@@ -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
|
||||
+4
-4
@@ -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
|
||||
@@ -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
|
||||
+5
-8
@@ -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
|
||||
+66
-3
@@ -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
|
||||
@@ -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);
|
||||
```
|
||||
+73
-6
@@ -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
|
||||
+54
-5
@@ -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
|
||||
@@ -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
|
||||
+5
-8
@@ -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
|
||||
@@ -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
|
||||
+11
-14
@@ -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
|
||||
+8
-11
@@ -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
|
||||
+5
-8
@@ -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 →
|
||||
</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 →
|
||||
</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
|
||||
@@ -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) |
|
||||
+7
-10
@@ -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
|
||||
+7
-10
@@ -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
|
||||
+9
-12
@@ -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 { /* ... */ }
|
||||
}
|
||||
```
|
||||
@@ -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)
|
||||
);
|
||||
```
|
||||
@@ -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 | 10–20× 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
|
||||
+49
-33
@@ -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
|
||||
+36
-18
@@ -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.
|
||||
@@ -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
|
||||
+6
-9
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user