feat: adding new skills, including testing patterns and methodologies, along with bundled resources for better usability.

This commit is contained in:
duthaho
2026-03-30 12:18:00 +07:00
parent 0ff5ae4082
commit 7fa9a48c6c
89 changed files with 25808 additions and 923 deletions
+30 -8
View File
@@ -335,16 +335,34 @@ For strict TDD enforcement (no production code without failing test):
Enable mandatory verification before completion claims:
- Reference: `.claude/skills/methodology/verification-before-completion/SKILL.md`
### Available Methodology Skills
### Available Skills
| Category | Skills |
|----------|--------|
| Planning | brainstorming, writing-plans, executing-plans |
| Testing | test-driven-development, verification-before-completion, testing-anti-patterns |
| Debugging | systematic-debugging, root-cause-tracing, defense-in-depth |
| Collaboration | dispatching-parallel-agents, requesting-code-review, receiving-code-review, finishing-development-branch |
| **Languages** | python, typescript, javascript |
| **Frameworks** | fastapi, django, nextjs, react |
| **Databases** | postgresql, mongodb |
| **DevOps** | docker, github-actions |
| **Frontend** | tailwind, shadcn-ui |
| **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 |
| **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 - Reasoning** | sequential-thinking |
Skills location: `.claude/skills/methodology/`
Skills location: `.claude/skills/`
Each skill includes:
- YAML frontmatter with trigger description
- "When to Use" / "When NOT to Use" sections
- Core patterns with code examples
- Best practices and common pitfalls
- Bundled reference docs, templates, and scripts
### Sequential Thinking
@@ -436,6 +454,10 @@ pnpm install
## Kit Version
- **Claude Kit Version**: 2.0.0
- **Last Updated**: 2025-01-29
- **Claude Kit Version**: 3.0.0
- **Last Updated**: 2026-03-30
- **Compatible with**: Claude Code 1.0+
- **Total Skills**: 38 (with YAML frontmatter, bundled resources)
- **Total Commands**: 27+
- **Total Agents**: 20
- **Behavioral Modes**: 7
+799 -54
View File
@@ -1,92 +1,837 @@
# OpenAPI
---
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.
---
## Description
OpenAPI/Swagger specification patterns for REST API documentation.
# 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
### Basic Specification
### 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.0.3
openapi: 3.1.0
info:
title: My API
version: 1.0.0
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:
summary: List users
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: Success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
description: Webhook received successfully.
components:
schemas:
User:
WebhookEventBase:
type: object
required:
- id
- type
- createdAt
properties:
id:
type: string
email:
format: uuid
type:
type: string
format: email
required:
- id
- email
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
```
### Request Body
Document a shared `WebhookEventBase` so all event payloads have a consistent
envelope with `id`, `type`, and `createdAt`.
```yaml
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUser'
example:
email: user@example.com
name: John
```
### Error Responses
```yaml
responses:
'400':
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
```
---
## Best Practices
1. Use $ref for reusable schemas
2. Include examples
3. Document all error responses
4. Use proper HTTP status codes
5. Add security schemes
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
- **Missing examples**: Add realistic examples
- **No error docs**: Document all errors
- **Inconsistent naming**: Use consistent conventions
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
@@ -0,0 +1,175 @@
# HTTP Status Codes for REST APIs
Quick reference for selecting the correct HTTP status code in REST API responses.
---
## 2xx Success
| Code | Name | When to Use |
|------|------|-------------|
| `200` | OK | General success. GET returns data, PUT/PATCH returns updated resource. |
| `201` | Created | POST successfully created a resource. Include `Location` header. |
| `202` | Accepted | Request accepted for async processing. Return a job/task ID. |
| `204` | No Content | DELETE success or PUT/PATCH with no response body needed. |
**Guidelines:**
- `200` is the default success response for GET, PUT, PATCH
- `201` must be used when a new resource is created (POST)
- `204` is preferred for DELETE (no body to return)
- `202` signals "we got it, processing later" -- return a status URL
```json
// 201 Created response
{
"id": "usr_abc123",
"name": "Jane Doe",
"created_at": "2025-01-15T10:30:00Z"
}
// Header: Location: /api/v1/users/usr_abc123
```
---
## 3xx Redirection
| Code | Name | When to Use |
|------|------|-------------|
| `301` | Moved Permanently | Resource URL changed permanently. Clients should update bookmarks. |
| `302` | Found | Temporary redirect. Original URL still valid. |
| `304` | Not Modified | Conditional GET -- resource unchanged since `If-None-Match`/`If-Modified-Since`. |
| `307` | Temporary Redirect | Like 302 but preserves HTTP method. Use for API redirects. |
| `308` | Permanent Redirect | Like 301 but preserves HTTP method. |
**Guidelines:**
- Prefer `307`/`308` over `302`/`301` in APIs (method preservation)
- `304` reduces bandwidth when clients cache responses
- Always include `Location` header with redirect responses
---
## 4xx Client Errors
| Code | Name | When to Use |
|------|------|-------------|
| `400` | Bad Request | Malformed syntax, invalid JSON, failed validation. |
| `401` | Unauthorized | Missing or invalid authentication credentials. |
| `403` | Forbidden | Authenticated but lacks permission for this resource. |
| `404` | Not Found | Resource does not exist at this URL. |
| `405` | Method Not Allowed | HTTP method not supported on this endpoint. |
| `409` | Conflict | Request conflicts with current state (duplicate, version mismatch). |
| `410` | Gone | Resource existed but has been permanently deleted. |
| `415` | Unsupported Media Type | Content-Type header not supported. |
| `422` | Unprocessable Entity | Valid JSON but semantically invalid (business rule violation). |
| `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
```json
// 422 Unprocessable Entity
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{ "field": "email", "message": "Email already registered" },
{ "field": "age", "message": "Must be 18 or older" }
]
}
}
```
```json
// 429 Too Many Requests
// Header: Retry-After: 60
{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Try again in 60 seconds."
}
}
```
---
## 5xx Server Errors
| Code | Name | When to Use |
|------|------|-------------|
| `500` | Internal Server Error | Unhandled exception. Generic server failure. |
| `501` | Not Implemented | Endpoint exists but functionality not built yet. |
| `502` | Bad Gateway | Upstream service returned invalid response. |
| `503` | Service Unavailable | Server overloaded or in maintenance. Include `Retry-After`. |
| `504` | Gateway Timeout | Upstream service did not respond in time. |
**Guidelines:**
- `500` should never expose stack traces in production
- `503` should include `Retry-After` header and a maintenance message
- Log all 5xx errors with request context for debugging
- Return a consistent error body format for all 5xx responses
```json
// 500 Internal Server Error (production)
{
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred. Please try again.",
"request_id": "req_7f3a9b2c"
}
}
```
---
## Decision Flowchart
```
Request received
|
+-- Is it valid syntax? -- NO --> 400 Bad Request
|
+-- Is caller authenticated? -- NO --> 401 Unauthorized
|
+-- Is caller authorized? -- NO --> 403 Forbidden
|
+-- Does resource exist? -- NO --> 404 Not Found
|
+-- Is it rate-limited? -- YES --> 429 Too Many Requests
|
+-- Does it pass business rules? -- NO --> 422 Unprocessable Entity
|
+-- Any conflicts? -- YES --> 409 Conflict
|
+-- Server error? -- YES --> 500 Internal Server Error
|
+-- Success!
GET --> 200 OK
POST --> 201 Created
PUT --> 200 OK
PATCH --> 200 OK
DELETE --> 204 No Content
```
---
## Standard Error Response Format
Use a consistent structure across all error responses:
```json
{
"error": {
"code": "MACHINE_READABLE_CODE",
"message": "Human-readable description",
"details": [],
"request_id": "req_..."
}
}
```
*Reference: [RFC 9110 - HTTP Semantics](https://httpwg.org/specs/rfc9110.html), [RFC 9457 - Problem Details](https://www.rfc-editor.org/rfc/rfc9457)*
@@ -0,0 +1,196 @@
# REST API Naming Conventions
Guidelines for consistent, predictable REST endpoint design.
---
## Core Rules
1. **Use plural nouns** for resource collections
2. **Use kebab-case** for multi-word resources
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
---
## Resource Naming
| Pattern | Example | Notes |
|---------|---------|-------|
| Collection | `/users` | Plural noun |
| Single resource | `/users/{id}` | Path parameter |
| Multi-word resource | `/order-items` | Kebab-case |
| Nested resource | `/users/{id}/orders` | Parent-child relationship |
| Deep nesting (avoid) | `/users/{id}/orders/{oid}/items` | Max 2 levels deep |
| Singleton sub-resource | `/users/{id}/profile` | One-to-one relationship |
### Good
```
GET /users
GET /users/123
POST /users
PUT /users/123
DELETE /users/123
GET /users/123/orders
GET /order-items
GET /user-profiles/123
```
### Bad
```
GET /getUsers # verb in URL
GET /user/123 # singular collection
GET /Users # uppercase
POST /users/create # redundant verb
GET /user_profiles # snake_case
DELETE /users/123/delete # verb in URL
```
---
## CRUD Mapping
| Action | Method | Endpoint | Request Body | Response |
|--------|--------|----------|-------------|----------|
| List | `GET` | `/resources` | None | `200` + array |
| Create | `POST` | `/resources` | Resource data | `201` + created |
| Read | `GET` | `/resources/{id}` | None | `200` + object |
| Update (full) | `PUT` | `/resources/{id}` | Full resource | `200` + updated |
| Update (partial) | `PATCH` | `/resources/{id}` | Partial data | `200` + updated |
| Delete | `DELETE` | `/resources/{id}` | None | `204` |
---
## Nested Resources
Use nesting to express clear parent-child relationships.
```
# User's orders (user owns orders)
GET /users/{userId}/orders
POST /users/{userId}/orders
# Order's line items
GET /orders/{orderId}/items
```
**When to nest vs. top-level:**
| Scenario | Approach | Example |
|----------|----------|---------|
| Resource only exists under parent | Nest | `/users/{id}/sessions` |
| Resource is independently accessible | Top-level with filter | `/orders?user_id=123` |
| Shallow relationship | Top-level | `/comments?post_id=456` |
**Rule of thumb:** Never nest more than 2 levels. Use query parameters or top-level endpoints instead.
```
# Too deep -- avoid
GET /users/{id}/orders/{oid}/items/{iid}/reviews
# Better alternatives
GET /order-items/{iid}/reviews
GET /reviews?order_item_id={iid}
```
---
## Query Parameters
### Filtering
```
GET /products?category=electronics&brand=acme
GET /users?status=active&role=admin
GET /orders?created_after=2025-01-01&created_before=2025-02-01
```
| Convention | Example |
|-----------|---------|
| Exact match | `?status=active` |
| Date range | `?created_after=2025-01-01` |
| Multiple values | `?status=active,pending` |
| Search | `?q=search+term` |
### Sorting
```
GET /products?sort=price # ascending (default)
GET /products?sort=-price # descending (prefix -)
GET /products?sort=-created_at,name # multi-field
```
### Pagination
```
# Offset-based (simple, common)
GET /products?page=2&per_page=25
# Cursor-based (better for large datasets)
GET /products?cursor=eyJpZCI6MTAwfQ&limit=25
```
**Response envelope for paginated results:**
```json
{
"data": [...],
"pagination": {
"page": 2,
"per_page": 25,
"total": 150,
"total_pages": 6
}
}
```
### Field Selection
```
GET /users/123?fields=id,name,email
```
---
## Non-CRUD Actions
Some operations do not map cleanly to CRUD. Use sub-resources with a noun or POST with an action resource.
| Action | Approach | Example |
|--------|----------|---------|
| Send an email | POST to action resource | `POST /users/{id}/verification-email` |
| Archive | PATCH with status | `PATCH /orders/{id} { "status": "archived" }` |
| Bulk delete | POST to action | `POST /users/bulk-delete { "ids": [...] }` |
| Export | GET with format | `GET /reports/sales?format=csv` |
| Search (complex) | POST with body | `POST /products/search { "filters": {...} }` |
---
## Versioning
| Strategy | Example | Pros | Cons |
|----------|---------|------|------|
| URL path | `/api/v1/users` | Simple, explicit | URL pollution |
| Header | `Accept: application/vnd.api.v1+json` | Clean URLs | Hidden |
| Query param | `/users?version=1` | Easy to test | Caching issues |
**Recommended:** URL path versioning (`/api/v1/`) for public APIs due to simplicity.
---
## Summary Checklist
- [ ] Resources are plural nouns (`/users` not `/user`)
- [ ] URLs are kebab-case and lowercase
- [ ] No verbs in URLs
- [ ] Nesting limited to 2 levels
- [ ] Filtering uses query parameters
- [ ] Sorting supports `-field` for descending
- [ ] Pagination included on all list endpoints
- [ ] API version in URL path for public APIs
- [ ] Consistent error response format
*Reference: [Google API Design Guide](https://cloud.google.com/apis/design), [Microsoft REST Guidelines](https://github.com/microsoft/api-guidelines)*
@@ -0,0 +1,240 @@
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 }]
+544 -38
View File
@@ -1,73 +1,579 @@
---
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.
---
# MongoDB
## Description
MongoDB patterns including document design, queries, and aggregation.
## When to Use
- MongoDB database operations
- Document-based data modeling
- Aggregation pipelines
- Semi-structured or polymorphic data that varies per record
- Rapid prototyping where schema flexibility accelerates iteration
- Event logging, IoT telemetry, or content management systems
## When NOT to Use
- Relational-heavy data models with complex joins and foreign key constraints
- SQL-only projects where the entire stack is built around relational databases
- Simple key-value storage where Redis or a lightweight store is more appropriate
- Financial systems requiring multi-table ACID transactions as the norm
---
## Core Patterns
### Document Operations
### 1. Schema Design
```javascript
// Insert
db.users.insertOne({
email: 'user@example.com',
name: 'John',
createdAt: new Date()
});
The central decision in MongoDB modeling is **embed vs. reference**.
// Find
db.users.find({ active: true }).sort({ createdAt: -1 }).limit(20);
**Decision tree:**
// Update
db.users.updateOne(
{ _id: ObjectId('...') },
{ $set: { name: 'Jane' } }
);
```
Does the child data belong to exactly one parent?
YES --> Is the child array unbounded (could grow to thousands)?
YES --> Reference (separate collection)
NO --> Embed
NO --> Is it a many-to-many relationship?
YES --> Reference (with array of ObjectIds on one or both sides)
NO --> Reference
```
### Aggregation
**Embedding pattern -- best for data that is read together:**
```javascript
// User with embedded address and preferences
// Good: one read fetches everything the profile page needs
db.users.insertOne({
email: "user@example.com",
name: "Alice Chen",
address: {
street: "123 Main St",
city: "Portland",
state: "OR",
zip: "97201"
},
preferences: {
theme: "dark",
language: "en",
notifications: { email: true, push: false }
},
createdAt: new Date()
});
```
**Referencing pattern -- best for independent or unbounded data:**
```javascript
// Orders reference the user by ID
// Good: orders grow unboundedly, accessed independently
db.orders.insertOne({
userId: ObjectId("6651a..."),
status: "shipped",
totalCents: 4999,
items: [
{ sku: "WIDGET-001", name: "Blue Widget", qty: 2, priceCents: 1999 },
{ sku: "GADGET-010", name: "Mini Gadget", qty: 1, priceCents: 1001 }
],
placedAt: new Date()
});
```
**Denormalization pattern -- duplicate data to avoid frequent lookups:**
```javascript
// Store author name directly on the post (denormalized from users)
// Trade-off: faster reads, but updates to user name require updating all posts
db.posts.insertOne({
title: "Getting Started with MongoDB",
body: "...",
author: {
_id: ObjectId("6651a..."),
name: "Alice Chen" // denormalized -- must be updated if name changes
},
tags: ["mongodb", "tutorial"],
publishedAt: new Date()
});
```
**Polymorphic pattern -- different shapes in one collection:**
```javascript
// Events collection stores different event types
db.events.insertMany([
{
type: "page_view",
userId: ObjectId("6651a..."),
url: "/products/widget",
timestamp: new Date()
},
{
type: "purchase",
userId: ObjectId("6651a..."),
orderId: ObjectId("6651b..."),
totalCents: 4999,
timestamp: new Date()
}
]);
// Use a discriminator field (type) and query by it
```
**Schema validation -- enforce structure at the database level:**
```javascript
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["email", "name", "createdAt"],
properties: {
email: {
bsonType: "string",
pattern: "^.+@.+\\..+$",
description: "Must be a valid email"
},
name: {
bsonType: "string",
minLength: 1
},
role: {
enum: ["admin", "editor", "viewer"],
description: "Must be a valid role"
},
createdAt: { bsonType: "date" }
}
}
},
validationLevel: "strict",
validationAction: "error"
});
```
---
### 2. Aggregation Pipeline
Build complex data transformations as a sequence of stages.
```javascript
// Revenue report: total and average spend per user, last 30 days
db.orders.aggregate([
{ $match: { status: 'completed' } },
{ $group: {
_id: '$userId',
totalSpent: { $sum: '$amount' },
orderCount: { $count: {} }
// Stage 1: filter to recent delivered orders
{ $match: {
status: "delivered",
placedAt: { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) }
}},
{ $sort: { totalSpent: -1 } }
// Stage 2: group by user
{ $group: {
_id: "$userId",
totalSpent: { $sum: "$totalCents" },
orderCount: { $sum: 1 },
avgOrderValue: { $avg: "$totalCents" }
}},
// Stage 3: sort by spend
{ $sort: { totalSpent: -1 } },
// Stage 4: limit to top 10
{ $limit: 10 },
// Stage 5: join user details
{ $lookup: {
from: "users",
localField: "_id",
foreignField: "_id",
as: "user"
}},
// Stage 6: flatten the joined array
{ $unwind: "$user" },
// Stage 7: reshape output
{ $project: {
_id: 0,
userName: "$user.name",
email: "$user.email",
totalSpent: 1,
orderCount: 1,
avgOrderValue: { $round: ["$avgOrderValue", 0] }
}}
]);
```
### Indexes
**$unwind -- flatten arrays into individual documents:**
```javascript
// Single field
// Expand order items to analyze product-level metrics
db.orders.aggregate([
{ $unwind: "$items" },
{ $group: {
_id: "$items.sku",
totalQty: { $sum: "$items.qty" },
totalRevenue: { $sum: { $multiply: ["$items.qty", "$items.priceCents"] } }
}},
{ $sort: { totalRevenue: -1 } }
]);
```
**$lookup with pipeline -- filtered/correlated joins:**
```javascript
// For each user, get their 3 most recent orders
db.users.aggregate([
{ $lookup: {
from: "orders",
let: { uid: "$_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$userId", "$$uid"] } } },
{ $sort: { placedAt: -1 } },
{ $limit: 3 },
{ $project: { status: 1, totalCents: 1, placedAt: 1 } }
],
as: "recentOrders"
}}
]);
```
**$facet -- run multiple aggregations in parallel:**
```javascript
// Dashboard: get summary stats and top products in one query
db.orders.aggregate([
{ $match: { status: "delivered" } },
{ $facet: {
summary: [
{ $group: {
_id: null,
totalRevenue: { $sum: "$totalCents" },
totalOrders: { $sum: 1 }
}}
],
topProducts: [
{ $unwind: "$items" },
{ $group: { _id: "$items.sku", sold: { $sum: "$items.qty" } } },
{ $sort: { sold: -1 } },
{ $limit: 5 }
],
monthlyTrend: [
{ $group: {
_id: { $dateToString: { format: "%Y-%m", date: "$placedAt" } },
revenue: { $sum: "$totalCents" }
}},
{ $sort: { _id: 1 } }
]
}}
]);
```
---
### 3. Index Strategies
```javascript
// Single field index -- most common
db.users.createIndex({ email: 1 }, { unique: true });
// Compound
db.posts.createIndex({ userId: 1, createdAt: -1 });
// Compound index -- order matters, follows the ESR rule:
// Equality fields first, Sort fields next, Range fields last
db.orders.createIndex({ status: 1, placedAt: -1 });
// Supports: find({status: "pending"}).sort({placedAt: -1})
// Also supports: find({status: "pending"}) alone (prefix)
// Multikey index -- automatically indexes each array element
db.posts.createIndex({ tags: 1 });
// Supports: find({ tags: "mongodb" })
// Text index -- basic full-text search
db.posts.createIndex(
{ title: "text", body: "text" },
{ weights: { title: 10, body: 1 }, name: "posts_text_search" }
);
// Usage:
db.posts.find(
{ $text: { $search: "mongodb aggregation" } },
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } });
// TTL index -- auto-delete documents after expiry
db.sessions.createIndex(
{ expiresAt: 1 },
{ expireAfterSeconds: 0 } // delete when expiresAt is in the past
);
// Documents must have a Date field; they are removed by a background task ~every 60s
// Partial index -- only index documents matching a filter
db.orders.createIndex(
{ placedAt: -1 },
{ partialFilterExpression: { status: "pending" } }
);
// Smaller index; only used when the query includes the filter condition
// Wildcard index -- for querying arbitrary keys in a sub-document
db.products.createIndex({ "attributes.$**": 1 });
// Supports: find({ "attributes.color": "red" }) without knowing keys in advance
// Collation -- case-insensitive sorting and matching
db.users.createIndex(
{ name: 1 },
{ collation: { locale: "en", strength: 2 } }
);
```
**The ESR rule for compound indexes:** order fields by **E**quality, **S**ort, **R**ange. This produces the most efficient index scans.
```javascript
// Query: find active orders for a user, sorted by date, in a date range
// Equality: userId, status
// Sort: placedAt
// Range: placedAt (but sort and range on same field -- sort wins)
db.orders.createIndex({ userId: 1, status: 1, placedAt: -1 });
```
---
### 4. Transactions
Multi-document transactions work across collections (requires replica set or sharded cluster).
```javascript
const session = client.startSession();
try {
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" },
readPreference: "primary"
});
const accounts = client.db("bank").collection("accounts");
// Transfer $50 from account A to account B
const fromAccount = await accounts.findOne(
{ _id: "account-A" },
{ session }
);
if (fromAccount.balanceCents < 5000) {
await session.abortTransaction();
throw new Error("Insufficient funds");
}
await accounts.updateOne(
{ _id: "account-A" },
{ $inc: { balanceCents: -5000 } },
{ session }
);
await accounts.updateOne(
{ _id: "account-B" },
{ $inc: { balanceCents: 5000 } },
{ session }
);
// Record the transfer in a separate collection -- still in the same tx
await client.db("bank").collection("transfers").insertOne({
from: "account-A",
to: "account-B",
amountCents: 5000,
timestamp: new Date()
}, { session });
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
```
**Guidelines:**
- Keep transactions short -- they hold locks and consume resources
- Design your schema to minimize the need for multi-document transactions
- Transactions have a default 60-second timeout (`maxTimeMS`)
- Retryable writes (`retryWrites=true` in connection string) handle transient errors automatically
---
### 5. Change Streams
Watch for real-time changes to collections, databases, or the entire deployment.
```javascript
// Watch a single collection for inserts and updates
const pipeline = [
{ $match: {
operationType: { $in: ["insert", "update"] },
"fullDocument.status": "urgent"
}}
];
const changeStream = db.collection("tickets").watch(pipeline, {
fullDocument: "updateLookup" // include the full document on updates
});
changeStream.on("change", (change) => {
console.log("Change detected:", change.operationType);
console.log("Document:", change.fullDocument);
console.log("Resume token:", change.resumeToken);
// Process the change (e.g., send notification, update cache)
notifyTeam(change.fullDocument);
});
// Handle errors and resume from last known position
changeStream.on("error", (error) => {
console.error("Change stream error:", error);
// Reconnect using the stored resume token
});
```
**Resumable pattern for production:**
```javascript
let resumeToken = await loadResumeTokenFromStorage();
async function watchWithResume(collection) {
const options = { fullDocument: "updateLookup" };
if (resumeToken) {
options.resumeAfter = resumeToken;
}
const stream = collection.watch([], options);
stream.on("change", async (change) => {
// Process change
await handleChange(change);
// Persist resume token so we can recover after restart
resumeToken = change._id;
await saveResumeTokenToStorage(resumeToken);
});
stream.on("error", async () => {
// Wait and reconnect
await new Promise(r => setTimeout(r, 5000));
watchWithResume(collection);
});
}
```
**Use cases:** real-time dashboards, cache invalidation, event-driven architectures, syncing data to search indexes (e.g., Elasticsearch).
---
### 6. Performance
#### Reading explain() output
```javascript
// Run explain to see the query plan
db.orders.find({
userId: ObjectId("6651a..."),
status: "pending"
}).sort({ placedAt: -1 }).explain("executionStats");
```
**Key fields in executionStats:**
| Field | What to look for |
|-------|-----------------|
| `winningPlan.stage` | `IXSCAN` good, `COLLSCAN` bad (full collection scan) |
| `totalKeysExamined` | Should be close to `nReturned` (no wasted index scans) |
| `totalDocsExamined` | Should be close to `nReturned` (no wasted document reads) |
| `executionTimeMillis` | Overall query time |
| `rejectedPlans` | Shows alternatives the optimizer considered |
**Covered queries -- answered entirely from the index:**
```javascript
// Create an index that covers the query
db.orders.createIndex({ userId: 1, status: 1, totalCents: 1 });
// This query only needs fields in the index -- no document fetch
db.orders.find(
{ userId: ObjectId("6651a..."), status: "delivered" },
{ _id: 0, totalCents: 1 } // projection must exclude _id and only include indexed fields
);
// explain() will show: "totalDocsExamined": 0
```
**Projection optimization -- fetch only what you need:**
```javascript
// BAD: fetches entire document including large body field
const posts = await db.posts.find({ author: userId }).toArray();
// GOOD: only fetch fields needed for the list view
const posts = await db.posts.find(
{ author: userId },
{ projection: { title: 1, publishedAt: 1, tags: 1 } }
).toArray();
```
**Bulk operations for write-heavy workloads:**
```javascript
const bulk = db.products.initializeUnorderedBulkOp();
for (const update of priceUpdates) {
bulk.find({ sku: update.sku })
.updateOne({ $set: { priceCents: update.newPrice, updatedAt: new Date() } });
}
const result = await bulk.execute();
console.log(`Modified: ${result.nModified}, Errors: ${result.getWriteErrorCount()}`);
```
---
## Best Practices
1. Embed frequently accessed data
2. Use references for large/independent data
3. Create indexes for query patterns
4. Use aggregation for complex queries
5. Avoid unbounded arrays
1. **Design schema around query patterns, not data relationships.** Ask "how will I read this data?" before "how does this data relate?" Embed data that is always fetched together; reference data accessed independently.
2. **Use the ESR rule for compound indexes.** Order index fields by Equality, Sort, Range. This maximizes the index's usefulness and minimizes keys examined.
3. **Set read/write concerns appropriately.** Use `w: "majority"` and `readConcern: "majority"` for data that must survive failovers. Use `w: 1` for non-critical writes where speed matters more than durability.
4. **Use projection to limit returned fields.** Transferring large documents over the network when you only need two fields wastes bandwidth and memory. Always project.
5. **Avoid unbounded array growth.** An embedded array that can grow to thousands of elements bloats the document (16 MB max) and degrades performance. Move to a separate collection with a reference when the array exceeds ~100 elements.
6. **Use bulk operations for batch writes.** Individual `insertOne` or `updateOne` calls in a loop are slow. Batch them with `bulkWrite` or `initializeUnorderedBulkOp` for 10-50x throughput improvement.
7. **Enable retryable writes.** Add `retryWrites=true` to your connection string. This handles transient network errors and primary elections automatically without application-level retry logic.
8. **Monitor with database profiler and serverStatus.** Use `db.setProfilingLevel(1, { slowms: 100 })` to log slow queries. Check `db.serverStatus().opcounters` and `db.serverStatus().connections` for overall health.
## Common Pitfalls
- **Unbounded arrays**: Limit array size
- **Missing indexes**: Analyze query patterns
- **Over-embedding**: Consider data access patterns
1. **Treating MongoDB like a relational database.** Normalizing everything into separate collections and using `$lookup` for every query defeats the purpose. If you need heavy joins, PostgreSQL is likely a better fit. Design for embedding first.
2. **Missing indexes on query fields.** Every `find()`, `$match`, and `sort()` should be backed by an index. Use `db.collection.getIndexes()` and `explain()` to verify. A `COLLSCAN` on a large collection is almost always a bug.
3. **Ignoring the 16 MB document size limit.** Embedding unbounded arrays (comments, logs, events) will eventually hit this wall, crashing writes. Use the bucket pattern (fixed-size sub-documents) or reference a separate collection.
4. **Not using readPreference for read-heavy workloads.** By default all reads go to the primary. For analytics or non-critical reads, use `readPreference: "secondaryPreferred"` to distribute load across replicas.
5. **Forgetting that updates replace matched array elements, not all of them.** Using `$set` on a matched array element with positional `$` only updates the first match. Use `$[]` for all elements or `$[<identifier>]` with `arrayFilters` for conditional updates:
```javascript
// Update price for a specific item in all orders
db.orders.updateMany(
{ "items.sku": "WIDGET-001" },
{ $set: { "items.$[item].priceCents": 2499 } },
{ arrayFilters: [{ "item.sku": "WIDGET-001" }] }
);
```
6. **Running aggregation pipelines without early $match.** Always filter as early as possible in the pipeline. A `$group` or `$unwind` before `$match` processes the entire collection unnecessarily. Put `$match` first to leverage indexes and reduce documents flowing through subsequent stages.
## 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
@@ -0,0 +1,237 @@
# 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).
+579 -36
View File
@@ -1,69 +1,612 @@
---
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.
---
# PostgreSQL
## Description
PostgreSQL database patterns including queries, indexing, and optimization.
## When to Use
- PostgreSQL database operations
- SQL query optimization
- Schema design
- Schema design and migrations
- JSONB document storage within a relational model
- Full-text search without a dedicated search engine
- Complex analytical queries with window functions and CTEs
## When NOT to Use
- NoSQL-only projects where no relational database is involved
- In-memory databases like Redis or SQLite used purely for caching or ephemeral storage
- File-based storage scenarios that do not require a database engine
---
## Core Patterns
### Basic Queries
### 1. Schema Design
Design tables with explicit constraints, proper types, and clear relationships.
```sql
-- Select with filtering
SELECT id, name, email
FROM users
WHERE active = true
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;
-- Enums for constrained value sets
CREATE TYPE user_role AS ENUM ('admin', 'editor', 'viewer');
CREATE TYPE order_status AS ENUM ('pending', 'processing', 'shipped', 'delivered', 'cancelled');
-- Join
SELECT u.*, COUNT(p.id) as post_count
-- Composite types for reusable structures
CREATE TYPE address AS (
street TEXT,
city TEXT,
state TEXT,
zip VARCHAR(10)
);
-- Users table with constraints
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL CHECK (char_length(name) >= 1),
role user_role NOT NULL DEFAULT 'viewer',
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Organizations with self-referencing hierarchy
CREATE TABLE organizations (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
parent_id BIGINT REFERENCES organizations(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Membership join table with composite primary key
CREATE TABLE org_memberships (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
org_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
role user_role NOT NULL DEFAULT 'viewer',
joined_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, org_id)
);
-- Orders with foreign keys, check constraints, and enum status
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
status order_status NOT NULL DEFAULT 'pending',
total_cents BIGINT NOT NULL CHECK (total_cents >= 0),
shipping address,
items JSONB NOT NULL DEFAULT '[]',
placed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Auto-update updated_at with a trigger
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
```
**Key principles:**
- Use `BIGINT GENERATED ALWAYS AS IDENTITY` over `SERIAL` for new projects
- Use `TIMESTAMPTZ` (not `TIMESTAMP`) to store times with timezone awareness
- Prefer `TEXT` over `VARCHAR(n)` unless a hard length limit is business-critical
- Add `ON DELETE` actions on every foreign key (CASCADE, RESTRICT, or SET NULL)
- Use `CHECK` constraints for business rules that live at the data level
---
### 2. Index Strategy
Choose the right index type based on your query patterns.
**Decision guide:**
| Query Pattern | Index Type | Example |
|---------------|-----------|---------|
| Equality (`=`) and range (`<`, `>`, `BETWEEN`) | B-tree (default) | `WHERE created_at > '2025-01-01'` |
| Array containment (`@>`), JSONB queries | GIN | `WHERE tags @> '{postgres}'` |
| Full-text search (`@@`) | GIN | `WHERE to_tsvector(body) @@ query` |
| Geometry, range overlap | GiST | `WHERE location <-> point '(40.7,-74.0)' < 0.01` |
| Filtered subset of rows | Partial | `WHERE active = true` |
| Index-only scans (no heap lookup) | Covering (INCLUDE) | Frequently selected columns |
```sql
-- B-tree: default, good for equality and range
CREATE INDEX idx_orders_placed_at ON orders(placed_at DESC);
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- GIN: arrays and JSONB containment
CREATE INDEX idx_users_metadata ON users USING GIN (metadata);
CREATE INDEX idx_orders_items ON orders USING GIN (items jsonb_path_ops);
-- GIN: full-text search
ALTER TABLE articles ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(body, '')), 'B')
) STORED;
CREATE INDEX idx_articles_search ON articles USING GIN (search_vector);
-- Full-text search query
SELECT id, title, ts_rank(search_vector, query) AS rank
FROM articles, plainto_tsquery('english', 'database optimization') AS query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 20;
-- GiST: geometry and range types
CREATE INDEX idx_events_duration ON events USING GiST (
tstzrange(starts_at, ends_at)
);
-- Find overlapping events
SELECT * FROM events
WHERE tstzrange(starts_at, ends_at) && tstzrange('2025-06-01', '2025-06-02');
-- Partial index: only index rows you actually query
CREATE INDEX idx_orders_pending ON orders(placed_at)
WHERE status = 'pending';
-- Covering index: avoids heap lookup for common queries
CREATE INDEX idx_users_email_covering ON users(email)
INCLUDE (name, role);
-- This query can now be answered entirely from the index
SELECT name, role FROM users WHERE email = 'user@example.com';
```
**When to add an index:** Run `EXPLAIN ANALYZE` first. Add an index when you see sequential scans on large tables with selective WHERE clauses. Do not index columns with very low cardinality (e.g., a boolean on a small table) unless combined with other columns.
---
### 3. Query Optimization
#### Reading EXPLAIN ANALYZE
```sql
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
GROUP BY u.id;
JOIN orders o ON o.user_id = u.id
WHERE o.placed_at > now() - INTERVAL '30 days'
GROUP BY u.id, u.name
ORDER BY order_count DESC
LIMIT 10;
```
### Indexes
**What to look for in the output:**
- **Seq Scan on large tables** -- add an index or rewrite the WHERE clause
- **Nested Loop with high row counts** -- consider a Hash Join (may need more `work_mem`)
- **actual rows far exceeding estimated rows** -- run `ANALYZE tablename` to update statistics
- **Buffers: shared read** large numbers -- data not cached, check `shared_buffers` sizing
- **Sort Method: external merge** -- increase `work_mem` for this query
#### Common Query Rewrites
```sql
-- Single column index
CREATE INDEX idx_users_email ON users(email);
-- BAD: correlated subquery runs once per row
SELECT u.name,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_count
FROM users u;
-- Composite index
CREATE INDEX idx_posts_user_date ON posts(user_id, created_at DESC);
-- GOOD: single pass with JOIN + GROUP BY
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name;
-- Partial index
CREATE INDEX idx_active_users ON users(email) WHERE active = true;
-- BAD: OR on different columns defeats index usage
SELECT * FROM orders WHERE user_id = 5 OR status = 'pending';
-- GOOD: UNION ALL lets each branch use its own index
SELECT * FROM orders WHERE user_id = 5
UNION ALL
SELECT * FROM orders WHERE status = 'pending' AND user_id != 5;
-- BAD: function call on indexed column prevents index use
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';
-- GOOD: expression index or use citext
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
-- or better: define email as CITEXT type
-- Avoiding N+1: fetch users and their latest order in one query
SELECT DISTINCT ON (u.id)
u.id, u.name, o.id AS latest_order_id, o.total_cents, o.placed_at
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
ORDER BY u.id, o.placed_at DESC;
```
### Migrations
---
### 4. Migrations
Follow the up/down pattern and plan for zero-downtime deployments.
```sql
-- Add column with default
ALTER TABLE users ADD COLUMN role VARCHAR(50) DEFAULT 'user';
-- ============================================
-- Migration: 20250601_001_add_user_preferences
-- ============================================
-- Add constraint
ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email);
-- UP
ALTER TABLE users ADD COLUMN preferences JSONB DEFAULT '{}';
-- Create index CONCURRENTLY to avoid locking the table
CREATE INDEX CONCURRENTLY idx_users_preferences
ON users USING GIN (preferences);
-- DOWN
DROP INDEX IF EXISTS idx_users_preferences;
ALTER TABLE users DROP COLUMN IF EXISTS preferences;
```
**Safe vs unsafe operations:**
| Operation | Safe? | Notes |
|-----------|-------|-------|
| ADD COLUMN (nullable or with volatile default) | Yes | Instant in PG 11+ with non-volatile default too |
| ADD COLUMN NOT NULL without default | No | Fails if rows exist; add nullable first, backfill, then set NOT NULL |
| DROP COLUMN | Mostly | Quick, but ORM queries may break if they SELECT * |
| RENAME COLUMN | Dangerous | Breaks all queries referencing old name; use a transition period |
| ADD INDEX | Safe with CONCURRENTLY | Without CONCURRENTLY, locks writes for duration |
| ADD CONSTRAINT (CHECK/FK) | Careful | Use NOT VALID then VALIDATE CONSTRAINT in two steps |
| Change column type | Dangerous | Rewrites entire table; use a new column + migration instead |
```sql
-- Zero-downtime: add NOT NULL constraint safely
-- Step 1: add column as nullable
ALTER TABLE users ADD COLUMN phone TEXT;
-- Step 2: backfill in batches
UPDATE users SET phone = '' WHERE phone IS NULL AND id BETWEEN 1 AND 10000;
UPDATE users SET phone = '' WHERE phone IS NULL AND id BETWEEN 10001 AND 20000;
-- ... continue in batches
-- Step 3: add constraint without full table lock
ALTER TABLE users ADD CONSTRAINT users_phone_not_null
CHECK (phone IS NOT NULL) NOT VALID;
-- Step 4: validate (scans table but allows concurrent writes)
ALTER TABLE users VALIDATE CONSTRAINT users_phone_not_null;
-- Step 5: optionally convert to proper NOT NULL
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
ALTER TABLE users DROP CONSTRAINT users_phone_not_null;
```
---
### 5. JSON/JSONB
Use JSONB for semi-structured data that lives alongside relational columns.
**When to use JSONB:**
- User preferences, settings, or metadata with varying keys
- API response caching or event payloads
- Flexible attributes that differ per row
**When NOT to use JSONB:**
- Data you regularly JOIN on or use in WHERE clauses across tables -- normalize it
- Data that has a fixed, well-known schema -- use proper columns
```sql
-- Querying JSONB: operators
-- -> returns JSONB element (keeps type)
-- ->> returns TEXT value
-- @> containment (left contains right)
-- ? key exists
-- Get a nested value
SELECT
metadata->>'department' AS department,
metadata->'settings'->>'theme' AS theme
FROM users
WHERE metadata @> '{"role": "admin"}';
-- Check if a key exists
SELECT * FROM users WHERE metadata ? 'avatar_url';
-- Query inside JSONB arrays
SELECT * FROM orders
WHERE items @> '[{"sku": "WIDGET-001"}]';
-- Update a nested JSONB field
UPDATE users
SET metadata = jsonb_set(metadata, '{settings,notifications}', '"email"')
WHERE id = 42;
-- Remove a key
UPDATE users
SET metadata = metadata - 'deprecated_field'
WHERE metadata ? 'deprecated_field';
-- Aggregate JSONB: expand array elements into rows
SELECT o.id, item->>'sku' AS sku, (item->>'qty')::int AS qty
FROM orders o, jsonb_array_elements(o.items) AS item
WHERE o.status = 'pending';
-- Index strategies for JSONB
-- General containment queries: GIN with jsonb_ops (default)
CREATE INDEX idx_users_metadata_gin ON users USING GIN (metadata);
-- Containment-only queries (smaller, faster index): jsonb_path_ops
CREATE INDEX idx_orders_items_path ON orders USING GIN (items jsonb_path_ops);
-- Specific key lookups: expression index on extracted value
CREATE INDEX idx_users_department ON users ((metadata->>'department'));
```
---
### 6. CTEs and Window Functions
#### Common Table Expressions (CTEs)
```sql
-- Readable multi-step query with CTEs
WITH monthly_revenue AS (
SELECT
date_trunc('month', placed_at) AS month,
SUM(total_cents) AS revenue_cents
FROM orders
WHERE status = 'delivered'
GROUP BY 1
),
revenue_with_growth AS (
SELECT
month,
revenue_cents,
LAG(revenue_cents) OVER (ORDER BY month) AS prev_month,
ROUND(
100.0 * (revenue_cents - LAG(revenue_cents) OVER (ORDER BY month))
/ NULLIF(LAG(revenue_cents) OVER (ORDER BY month), 0),
1
) AS growth_pct
FROM monthly_revenue
)
SELECT * FROM revenue_with_growth ORDER BY month DESC;
-- Recursive CTE: org hierarchy tree
WITH RECURSIVE org_tree AS (
-- Base case: top-level orgs
SELECT id, name, parent_id, 0 AS depth, name::TEXT AS path
FROM organizations
WHERE parent_id IS NULL
UNION ALL
-- Recursive step
SELECT o.id, o.name, o.parent_id, t.depth + 1, t.path || ' > ' || o.name
FROM organizations o
JOIN org_tree t ON o.parent_id = t.id
)
SELECT * FROM org_tree ORDER BY path;
```
#### Window Functions
```sql
-- ROW_NUMBER: assign rank within a partition
SELECT
user_id,
id AS order_id,
total_cents,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY placed_at DESC) AS rn
FROM orders;
-- Get each user's most recent order
SELECT * FROM (
SELECT
o.*,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY placed_at DESC) AS rn
FROM orders o
) sub WHERE rn = 1;
-- LAG/LEAD: compare with previous/next row
SELECT
placed_at::date AS order_date,
total_cents,
LAG(total_cents) OVER (ORDER BY placed_at) AS prev_order_total,
total_cents - LAG(total_cents) OVER (ORDER BY placed_at) AS diff
FROM orders
WHERE user_id = 42;
-- Running total
SELECT
placed_at::date AS order_date,
total_cents,
SUM(total_cents) OVER (
ORDER BY placed_at
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM orders
WHERE user_id = 42;
-- NTILE: divide rows into equal buckets (e.g., quartiles)
SELECT
user_id,
SUM(total_cents) AS lifetime_spend,
NTILE(4) OVER (ORDER BY SUM(total_cents) DESC) AS spend_quartile
FROM orders
GROUP BY user_id;
```
---
### 7. Transaction Isolation
PostgreSQL supports four isolation levels. The two most commonly used:
| Level | Dirty Read | Non-Repeatable Read | Phantom Read | Use Case |
|-------|-----------|-------------------|-------------|----------|
| READ COMMITTED (default) | No | Possible | Possible | Most OLTP workloads |
| REPEATABLE READ | No | No | No (in PG) | Reports, consistent snapshots |
| SERIALIZABLE | No | No | No | Financial transactions, inventory |
```sql
-- Default: READ COMMITTED
-- Each statement sees the latest committed data
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- SERIALIZABLE: full isolation, detects write conflicts
BEGIN ISOLATION LEVEL SERIALIZABLE;
-- Read current inventory
SELECT quantity FROM inventory WHERE sku = 'WIDGET-001';
-- Decrement if sufficient (PG will abort if concurrent tx conflicts)
UPDATE inventory SET quantity = quantity - 1 WHERE sku = 'WIDGET-001';
COMMIT;
-- If another SERIALIZABLE tx modified the same row, one will get:
-- ERROR: could not serialize access due to concurrent update
-- Your application must retry on serialization failure (SQLSTATE 40001)
-- Advisory locks for application-level coordination
SELECT pg_advisory_xact_lock(hashtext('process-user-' || '42'));
-- Lock is held until transaction ends; no table-level contention
```
**Guidelines:**
- Use READ COMMITTED for general CRUD operations
- Use SERIALIZABLE when correctness requires that concurrent transactions behave as if run sequentially (e.g., balance transfers, seat reservations)
- Always implement retry logic for serialization failures
- Keep transactions as short as possible to reduce contention
---
### 8. Connection Pooling
Direct PostgreSQL connections are expensive (~1-10 MB RAM each). Use a pooler.
**PgBouncer configuration (pgbouncer.ini):**
```ini
[databases]
myapp = host=127.0.0.1 port=5432 dbname=myapp
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
; Pool mode: transaction is best for most web apps
pool_mode = transaction
; Sizing: start conservative, tune with monitoring
default_pool_size = 20
max_client_conn = 200
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
; Timeouts
server_idle_timeout = 300
client_idle_timeout = 60
query_timeout = 30
```
**Pool sizing formula:**
```
optimal_pool_size = ((2 * cpu_cores) + effective_disk_spindles)
```
For a 4-core SSD server: `(2 * 4) + 1 = 9` connections is a good starting point. More connections does not mean more throughput -- too many causes contention.
**Pool modes:**
| Mode | Description | Caveats |
|------|-------------|---------|
| `transaction` | Connection returned after each transaction | Cannot use session-level features (LISTEN/NOTIFY, prepared statements, temp tables) |
| `session` | Connection held for entire client session | Fewer pooling benefits; use only when session features needed |
| `statement` | Connection returned after each statement | No multi-statement transactions; rarely used |
**Application-level pooling (Python example with asyncpg):**
```python
import asyncpg
pool = await asyncpg.create_pool(
dsn="postgresql://user:pass@localhost:6432/myapp",
min_size=5,
max_size=20,
max_inactive_connection_lifetime=300,
command_timeout=30,
)
async with pool.acquire() as conn:
rows = await conn.fetch("SELECT * FROM users WHERE active = true")
```
---
## Best Practices
1. Use indexes for filtered/sorted columns
2. Use EXPLAIN ANALYZE for slow queries
3. Avoid SELECT * in production
4. Use transactions for multiple operations
5. Use connection pooling
1. **Use parameterized queries everywhere.** Never concatenate user input into SQL strings. ORMs and query builders handle this, but verify in raw SQL contexts.
2. **Run ANALYZE after bulk data changes.** The query planner relies on statistics. After large imports or deletes, run `ANALYZE tablename` to update them.
3. **Prefer BIGINT for primary keys.** INTEGER (max ~2.1 billion) can be exhausted sooner than expected in high-write systems. BIGINT costs 4 extra bytes per row but avoids a painful migration later.
4. **Store money as integers (cents).** Floating-point arithmetic causes rounding errors. Use `BIGINT` for cents or `NUMERIC(19,4)` if sub-cent precision is needed.
5. **Add indexes for foreign keys.** PostgreSQL does not automatically index the child side of a foreign key. Without it, DELETE on the parent table triggers a sequential scan on the child.
6. **Use TIMESTAMPTZ, not TIMESTAMP.** `TIMESTAMP WITHOUT TIME ZONE` silently drops timezone info. Always use `TIMESTAMPTZ` and let the application control display timezone.
7. **Set statement_timeout for web requests.** Prevent runaway queries from holding connections: `SET statement_timeout = '5s';` at session start, or configure per-role in PostgreSQL.
8. **Monitor with pg_stat_statements.** Enable this extension to track query performance over time. The top queries by `total_exec_time` are your optimization targets.
```sql
-- Find slowest queries
SELECT
calls,
round(total_exec_time::numeric, 1) AS total_ms,
round(mean_exec_time::numeric, 1) AS mean_ms,
query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
```
## Common Pitfalls
- **N+1 queries**: Use JOINs or batch loading
- **Missing indexes**: Add indexes for WHERE/ORDER BY
- **Large transactions**: Keep transactions short
1. **N+1 queries from ORM lazy loading.** Loading a list of users and then accessing `user.orders` in a loop generates one query per user. Use eager loading (`joinedload` in SQLAlchemy, `select_related` in Django) or batch the query with a JOIN.
2. **Locking the table during migrations.** `ALTER TABLE ... ADD COLUMN NOT NULL DEFAULT 'x'` is safe in PG 11+, but `CREATE INDEX` without `CONCURRENTLY` locks writes. Always use `CREATE INDEX CONCURRENTLY` in production migrations.
3. **Bloated tables from UPDATE-heavy workloads.** PostgreSQL MVCC creates dead tuples on every UPDATE. If autovacuum cannot keep up, table size and query times grow. Monitor `pg_stat_user_tables.n_dead_tup` and tune autovacuum settings for hot tables.
4. **Using OFFSET for pagination on large datasets.** `OFFSET 100000` forces PG to scan and discard 100,000 rows. Use keyset pagination instead:
```sql
-- BAD: slow for deep pages
SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 100000;
-- GOOD: keyset pagination
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20;
```
5. **Ignoring connection limits.** Each PostgreSQL connection consumes RAM. Opening hundreds of direct connections (e.g., one per serverless function invocation) will exhaust `max_connections` and crash the server. Always use PgBouncer or an application-level pool.
6. **Storing large blobs in the database.** Files over a few KB should go in object storage (S3, R2). Store the URL/key in PostgreSQL. Large `bytea` or `TEXT` columns bloat the table, slow backups, and waste shared_buffers cache.
## 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
@@ -0,0 +1,173 @@
# 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;
```
@@ -0,0 +1,143 @@
-- =============================================================================
-- 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;
+606 -41
View File
@@ -1,70 +1,216 @@
---
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.
---
# Docker
## Description
Docker containerization including Dockerfiles, compose, and best practices.
## When to Use
- Containerizing applications
- Local development environments
- CI/CD pipelines
## When NOT to Use
- Serverless-only deployments where containers are not part of the architecture (e.g., pure AWS Lambda, Cloudflare Workers)
- Local development without containers where native tooling is preferred
- Simple scripts or utilities that do not need isolation or reproducible environments
---
## Core Patterns
### Multi-stage Dockerfile (Node.js)
### 1. Multi-Stage Builds
Multi-stage builds separate build-time dependencies from the runtime image, producing
smaller, more secure containers.
#### Python (builder + slim runtime)
```dockerfile
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---- Build stage ----
FROM python:3.12-slim AS builder
# Production stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
```
WORKDIR /build
### Python Dockerfile
# Install build-only dependencies (gcc, etc.) needed by some wheels
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc libpq-dev && \
rm -rf /var/lib/apt/lists/*
```dockerfile
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# ---- Runtime stage ----
FROM python:3.12-slim
WORKDIR /app
# Install dependencies first (caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy only the installed packages from the builder
COPY --from=builder /install /usr/local
COPY . .
# Copy application code
COPY src/ ./src/
COPY main.py .
# Run as non-root
RUN addgroup --system app && adduser --system --ingroup app app
USER app
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
```
### Docker Compose
#### Node.js (build + nginx/alpine)
```dockerfile
# ---- Build stage ----
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies first for layer caching
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
# Copy source and build
COPY tsconfig.json ./
COPY src/ ./src/
COPY public/ ./public/
RUN pnpm build
# ---- Runtime stage (static site served by nginx) ----
FROM nginx:1.27-alpine
# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy built assets from builder
COPY --from=builder /app/dist /usr/share/nginx/html
# Run as non-root
RUN chown -R nginx:nginx /usr/share/nginx/html && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx && \
touch /var/run/nginx.pid && \
chown -R nginx:nginx /var/run/nginx.pid
USER nginx
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1
CMD ["nginx", "-g", "daemon off;"]
```
#### Node.js (API server with alpine runtime)
```dockerfile
# ---- Build stage ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY tsconfig.json ./
COPY src/ ./src/
RUN pnpm build
# Prune dev dependencies for a lighter production node_modules
RUN pnpm prune --prod
# ---- Runtime stage ----
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
RUN addgroup -S app && adduser -S app -G app
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
```
#### Go (build + scratch)
```dockerfile
# ---- Build stage ----
FROM golang:1.22-alpine AS builder
WORKDIR /build
# Download dependencies first for caching
COPY go.mod go.sum ./
RUN go mod download
# Copy source and build a static binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server
# ---- Runtime stage (scratch = empty image) ----
FROM scratch
# Copy CA certificates for HTTPS calls
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy the static binary
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
```
---
### 2. Docker Compose for Development
A full-featured Compose file with services, volumes, networks, healthchecks, and
environment variable management.
```yaml
version: '3.8'
services:
app:
build: .
build:
context: .
dockerfile: Dockerfile
target: builder # Use builder stage for dev with hot-reload
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/app
NODE_ENV: development
DATABASE_URL: postgresql://user:pass@db:5432/app
REDIS_URL: redis://redis:6379
env_file:
- .env.local # Local overrides (gitignored)
volumes:
- .:/app # Bind-mount source for hot-reload
- /app/node_modules # Anonymous volume to preserve node_modules
depends_on:
- db
db:
condition: service_healthy
redis:
condition: service_started
networks:
- backend
restart: unless-stopped
db:
image: postgres:16-alpine
@@ -72,23 +218,442 @@ services:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d app"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- backend
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
networks:
- backend
worker:
build:
context: .
dockerfile: Dockerfile.worker
environment:
DATABASE_URL: postgresql://user:pass@db:5432/app
REDIS_URL: redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- backend
restart: unless-stopped
volumes:
postgres_data:
redis_data:
networks:
backend:
driver: bridge
```
---
### 3. Layer Caching
Docker caches each layer. If a layer has not changed, every layer after it is also
cached. Order instructions from least-frequently-changed to most-frequently-changed.
#### Optimal instruction order
```dockerfile
FROM python:3.12-slim
WORKDIR /app
# 1. System dependencies (rarely change)
RUN apt-get update && apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
# 2. Dependency manifests (change when adding packages)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 3. Application code (changes most often)
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]
```
#### .dockerignore patterns
Always include a `.dockerignore` to keep the build context small and avoid leaking
secrets into layers.
```
# Version control
.git
.gitignore
# Dependencies (rebuilt inside container)
node_modules
__pycache__
*.pyc
.venv
venv
# Build output
dist
build
*.egg-info
# IDE and editor files
.vscode
.idea
*.swp
*.swo
# Environment and secrets
.env
.env.*
*.pem
*.key
# Docker files (not needed in context)
Dockerfile*
docker-compose*
.dockerignore
# Documentation and misc
README.md
CHANGELOG.md
LICENSE
docs/
```
---
### 4. Health Checks
Health checks let Docker (and orchestrators like Compose/Swarm/K8s) know when a
container is actually ready to serve traffic.
#### HTTP health check with curl
```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
```
#### HTTP health check with wget (alpine images without curl)
```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
```
#### TCP port check (for non-HTTP services)
```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD nc -z localhost 5432 || exit 1
```
#### Python-native check (no extra binaries needed)
```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
```
**Parameter reference:**
| Parameter | Description | Default |
|------------------|--------------------------------------------------|---------|
| `--interval` | Time between checks | 30s |
| `--timeout` | Max time for a single check | 30s |
| `--start-period` | Grace period before checks count as failures | 0s |
| `--retries` | Consecutive failures before marking unhealthy | 3 |
---
### 5. Security Hardening
#### Run as non-root user
```dockerfile
# Debian/Ubuntu based images
RUN addgroup --system app && adduser --system --ingroup app app
USER app
# Alpine based images
RUN addgroup -S app && adduser -S app -G app
USER app
```
#### Use minimal base images
| Base Image | Size | Use Case |
|--------------------|---------|---------------------------------------|
| `alpine` | ~5 MB | General minimal base |
| `*-slim` | ~50 MB | Debian-based with fewer packages |
| `distroless` | ~20 MB | Google's no-shell, no-package-manager |
| `scratch` | 0 MB | Static binaries only (Go, Rust) |
```dockerfile
# Distroless for Python
FROM gcr.io/distroless/python3-debian12
COPY --from=builder /app /app
CMD ["main.py"]
```
#### Never put secrets in image layers
```dockerfile
# BAD - secret is baked into image history
COPY .env /app/.env
RUN echo "API_KEY=secret123" >> /app/.env
# GOOD - pass secrets at runtime
CMD ["python", "main.py"]
# docker run -e API_KEY=secret123 myapp
# or docker run --env-file .env myapp
```
#### Multi-stage to exclude build tools
Build tools (compilers, package managers, source code) stay in the builder stage
and never reach the runtime image. This reduces attack surface and image size.
```dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build && pnpm prune --prod
FROM node:20-alpine
WORKDIR /app
# Only the built output and production deps are copied
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
CMD ["node", "dist/index.js"]
```
---
### 6. Environment Configuration
#### ARG vs ENV
| Directive | Available at | Persists in image | Use for |
|-----------|-------------|-------------------|-----------------------------|
| `ARG` | Build time | No | Build-time variables |
| `ENV` | Build + run | Yes | Runtime configuration |
```dockerfile
# ARG - only available during build
ARG NODE_ENV=production
ARG BUILD_VERSION=unknown
# ENV - available at build and runtime
ENV NODE_ENV=${NODE_ENV}
ENV APP_VERSION=${BUILD_VERSION}
# Build with: docker build --build-arg BUILD_VERSION=1.2.3 .
```
#### .env files with Compose
```yaml
services:
app:
build: .
# Single .env file
env_file:
- .env
# Multiple files (later files override earlier ones)
env_file:
- .env.defaults
- .env.local
# Inline environment variables (override env_file)
environment:
LOG_LEVEL: debug
DEBUG: "true"
```
#### Secrets management with Docker Compose
```yaml
services:
app:
build: .
secrets:
- db_password
- api_key
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
environment: API_KEY # Read from host environment
```
Inside the container, secrets are mounted at `/run/secrets/<name>` as files.
---
### 7. Networking
#### Bridge networks for service isolation
```yaml
services:
frontend:
build: ./frontend
ports:
- "3000:3000"
networks:
- frontend-net
- backend-net # Can reach the API
api:
build: ./api
ports:
- "8000:8000"
networks:
- backend-net # Reachable by frontend and workers
db:
image: postgres:16-alpine
networks:
- backend-net # Only reachable by api and workers
# No ports exposed to host
worker:
build: ./worker
networks:
- backend-net
networks:
frontend-net:
driver: bridge
backend-net:
driver: bridge
```
#### Service discovery
Within a Docker Compose network, services reach each other by **service name**
as the hostname.
```python
# In the api service, connect to db using its service name
DATABASE_URL = "postgresql://user:pass@db:5432/app"
# In the frontend service, call the api by service name
API_URL = "http://api:8000"
```
#### Exposing ports
```yaml
services:
app:
ports:
- "3000:3000" # host:container, binds to 0.0.0.0
- "127.0.0.1:3000:3000" # bind to localhost only (more secure)
expose:
- "3000" # expose to other containers only, not host
```
---
## Best Practices
1. Use multi-stage builds
2. Order commands for cache efficiency
3. Use .dockerignore
4. Run as non-root user
5. Use specific image tags
1. **Use multi-stage builds** -- Separate build dependencies from the runtime
image. The final image should contain only what is needed to run the
application.
2. **Pin image tags** -- Use `node:20.11-alpine` or a digest instead of
`node:latest` or `node:20`. Floating tags lead to unpredictable builds.
3. **Order instructions for cache efficiency** -- Copy dependency manifests and
install dependencies before copying application code. This ensures that code
changes do not invalidate the dependency layer cache.
4. **Use .dockerignore** -- Exclude `.git`, `node_modules`, `__pycache__`, `.env`
files, and anything not needed inside the container to keep the build context
small and avoid leaking secrets.
5. **Run as non-root** -- Add a `USER` instruction to run the process as an
unprivileged user. Never run production containers as root.
6. **Combine RUN commands** -- Merge related `RUN` instructions with `&&` to
reduce layers and always clean up apt/apk caches in the same layer that
installs packages.
7. **Use COPY instead of ADD** -- `COPY` is explicit and predictable. `ADD` has
implicit behaviors (tar extraction, URL fetching) that can surprise you.
8. **Set explicit HEALTHCHECK** -- Define health checks in the Dockerfile so
orchestrators know when the container is ready. This prevents routing traffic
to containers that are still starting up.
---
## Common Pitfalls
- **Large images**: Use slim/alpine bases
- **Cache busting**: Order COPY commands properly
- **Root user**: Add USER instruction
1. **Bloated images** -- Using full base images like `python:3.12` instead of
`python:3.12-slim` adds hundreds of megabytes. Always prefer slim or alpine
variants. Use multi-stage builds to exclude build tools.
2. **Cache invalidation by COPY order** -- Placing `COPY . .` before
`RUN pip install` means every code change reinstalls all dependencies. Always
copy the dependency manifest first, install, then copy the rest of the code.
3. **Running as root** -- Forgetting the `USER` instruction means the container
process runs as root. If the application is compromised, the attacker has full
control of the container filesystem.
4. **Secrets baked into layers** -- Using `COPY .env .` or `ARG` for secrets
embeds them in the image layer history. Anyone with access to the image can
extract them with `docker history`. Pass secrets at runtime via environment
variables or Docker secrets.
5. **Missing .dockerignore** -- Without a `.dockerignore`, the entire directory
(including `.git`, `node_modules`, `.env` files) is sent as build context.
This slows builds, increases image size, and risks leaking credentials.
6. **Ignoring healthchecks in Compose** -- Using `depends_on` without
`condition: service_healthy` means the dependent service starts as soon as
the database container starts, not when the database is actually ready to
accept connections. Always pair `depends_on` with healthchecks.
---
## 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
@@ -0,0 +1,196 @@
# 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 |
@@ -0,0 +1,93 @@
# =============================================================================
# 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"]
@@ -0,0 +1,78 @@
# =============================================================================
# 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"]
@@ -0,0 +1,100 @@
# =============================================================================
# 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:
+745 -29
View File
@@ -1,20 +1,33 @@
---
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.
---
# GitHub Actions
## Description
GitHub Actions CI/CD workflows including testing, building, and deployment.
## When to Use
- Setting up CI/CD pipelines
- Automating tests and builds
- Deployment automation
## When NOT to Use
- GitLab CI projects using `.gitlab-ci.yml` configuration
- Jenkins pipelines using Jenkinsfile or Groovy-based configuration
- CircleCI, Travis CI, or other non-GitHub CI/CD systems
---
## Core Patterns
### Basic CI Workflow
### 1. CI Pipeline
Complete CI workflow covering checkout, setup, install, lint, test, and build for
both Python and Node.js projects.
#### Node.js CI Pipeline
```yaml
name: CI
@@ -25,64 +38,767 @@ on:
pull_request:
branches: [main]
permissions:
contents: read
jobs:
test:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
node-version: "20"
cache: "pnpm"
- run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm typecheck
test:
name: Test
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm test -- --coverage
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 7
build:
name: Build
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 5
```
#### Python CI Pipeline
```yaml
name: CI - Python
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- run: pip install -r requirements-dev.txt
- run: ruff check .
- run: ruff format --check .
- run: mypy src/
test:
name: Test
runs-on: ubuntu-latest
needs: lint
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U test"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- run: pip install -r requirements.txt -r requirements-dev.txt
- name: Run tests
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
run: pytest -v --cov=src --cov-report=xml
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-xml
path: coverage.xml
retention-days: 7
```
---
### 2. Matrix Strategy
Matrix builds run the same job across multiple combinations of OS, language
version, or other variables.
#### OS and version matrix
```yaml
jobs:
test:
name: Test (${{ matrix.os }}, Node ${{ matrix.node }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: "npm"
- run: npm ci
- run: npm test
```
### Matrix Builds
#### Include and exclude
```yaml
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
node: [18, 20]
os: [ubuntu-latest, macos-latest, windows-latest]
python: ["3.11", "3.12"]
exclude:
# Skip Python 3.11 on Windows
- os: windows-latest
python: "3.11"
include:
# Add a specific combination with extra env
- os: ubuntu-latest
python: "3.13"
experimental: true
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental || false }}
steps:
- uses: actions/setup-node@v4
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
node-version: ${{ matrix.node }}
python-version: ${{ matrix.python }}
- run: pip install -r requirements.txt
- run: pytest
```
### Caching
---
### 3. Caching
Caching avoids re-downloading dependencies on every run. Use `hashFiles` to
generate cache keys from lockfiles so the cache invalidates when dependencies
change.
#### npm cache
```yaml
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: npm-
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
```
### Secrets
#### pnpm cache
```yaml
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
run: deploy --key "$API_KEY"
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: echo "store=$(pnpm store path)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.store }}
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-
```
#### pip cache
```yaml
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
pip-${{ runner.os }}-
```
#### Docker layer cache
```yaml
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
```
---
### 4. Reusable Workflows
Reusable workflows let you define a workflow once and call it from other
workflows, reducing duplication across repositories.
#### Defining a reusable workflow (`.github/workflows/reusable-test.yml`)
```yaml
name: Reusable Test Workflow
on:
workflow_call:
inputs:
node-version:
description: "Node.js version to use"
required: false
type: string
default: "20"
working-directory:
description: "Directory to run commands in"
required: false
type: string
default: "."
secrets:
NPM_TOKEN:
required: false
jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: "npm"
registry-url: "https://registry.npmjs.org"
- run: npm ci
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: npm test
```
#### Calling a reusable workflow
```yaml
name: CI
on:
push:
branches: [main]
jobs:
test-app:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: "20"
working-directory: "packages/app"
secrets: inherit # Pass all secrets to the called workflow
test-lib:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: "20"
working-directory: "packages/lib"
secrets: inherit
```
---
### 5. Composite Actions
Composite actions package multiple steps into a single reusable action. Unlike
reusable workflows, they run inline within the calling job.
#### Action definition (`.github/actions/setup-project/action.yml`)
```yaml
name: "Setup Project"
description: "Install Node.js, enable corepack, and install dependencies"
inputs:
node-version:
description: "Node.js version"
required: false
default: "20"
install-command:
description: "Command to install dependencies"
required: false
default: "pnpm install --frozen-lockfile"
runs:
using: "composite"
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Enable corepack
shell: bash
run: corepack enable
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: echo "store=$(pnpm store path)" >> "$GITHUB_OUTPUT"
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.store }}
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-
- name: Install dependencies
shell: bash
run: ${{ inputs.install-command }}
```
#### Using the composite action
```yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-project
with:
node-version: "20"
- run: pnpm build
```
---
### 6. Deployment
Deployment workflows with environment protection rules, manual approval gates,
and multi-stage promotion.
```yaml
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: "Target environment"
required: true
type: choice
options:
- staging
- production
permissions:
contents: read
deployments: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: corepack enable && pnpm install --frozen-lockfile
- run: pnpm build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: build
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- name: Deploy to staging
env:
DEPLOY_TOKEN: ${{ secrets.STAGING_DEPLOY_TOKEN }}
run: |
echo "Deploying to staging..."
# Replace with your actual deploy command
# e.g., aws s3 sync, rsync, wrangler publish, etc.
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: deploy-staging
if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production'
environment:
name: production
url: https://example.com
# Production environment should have required reviewers configured
# in GitHub Settings > Environments > production > Protection rules
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- name: Deploy to production
env:
DEPLOY_TOKEN: ${{ secrets.PRODUCTION_DEPLOY_TOKEN }}
run: |
echo "Deploying to production..."
```
---
### 7. Artifacts
Artifacts let you share data between jobs in the same workflow or persist build
outputs for later download.
#### Upload artifact
```yaml
- name: Upload test results
uses: actions/upload-artifact@v4
if: always() # Upload even if tests fail
with:
name: test-results-${{ matrix.os }}-${{ matrix.node }}
path: |
test-results/
coverage/
retention-days: 14
if-no-files-found: warn # warn, error, or ignore
```
#### Download artifact in another job
```yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
deploy:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- run: ls -la dist/
```
#### Download all artifacts
```yaml
- uses: actions/download-artifact@v4
with:
path: all-artifacts/
# Each artifact is placed in a subdirectory named after the artifact
```
---
### 8. Conditional Execution
Control when jobs and steps run using `if` expressions, job dependencies, and
path filters.
#### Path filters on triggers
```yaml
on:
push:
branches: [main]
paths:
- "src/**"
- "package.json"
- "pnpm-lock.yaml"
paths-ignore:
- "docs/**"
- "*.md"
```
#### Conditional jobs
```yaml
jobs:
changes:
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
backend:
- 'src/api/**'
- 'requirements*.txt'
frontend:
- 'src/web/**'
- 'package.json'
test-backend:
needs: changes
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt && pytest
test-frontend:
needs: changes
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
```
#### Conditional steps with if expressions
```yaml
steps:
- name: Run only on main branch
if: github.ref == 'refs/heads/main'
run: echo "On main"
- name: Run only on pull requests
if: github.event_name == 'pull_request'
run: echo "PR event"
- name: Run only when previous step failed
if: failure()
run: echo "Something failed"
- name: Always run (cleanup)
if: always()
run: echo "Cleanup"
- name: Run only when a label is present
if: contains(github.event.pull_request.labels.*.name, 'deploy')
run: echo "Deploy label found"
- name: Skip for dependabot
if: github.actor != 'dependabot[bot]'
run: npm test
```
#### Job dependencies
```yaml
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: echo "Linting..."
test:
runs-on: ubuntu-latest
steps:
- run: echo "Testing..."
# Runs after both lint and test succeed
deploy:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- run: echo "Deploying..."
# Runs even if test fails, but only after it completes
notify:
runs-on: ubuntu-latest
needs: [test]
if: always()
steps:
- run: echo "Test job status: ${{ needs.test.result }}"
```
---
## Best Practices
1. Use caching for dependencies
2. Run jobs in parallel when possible
3. Use environment secrets
4. Pin action versions
5. Add proper triggers
1. **Pin action versions with SHA** -- Use the full commit SHA instead of a
mutable tag: `actions/checkout@b4ffde65f...` (or at minimum a major version
tag like `@v4`). This prevents supply-chain attacks where a tag is moved.
2. **Use caching aggressively** -- Cache package manager stores (`~/.npm`,
pnpm store, `~/.cache/pip`) and Docker layers. A well-cached pipeline can
cut run times by 50-80%.
3. **Set minimal permissions** -- Add a top-level `permissions` block and grant
only what is needed. Default permissions are overly broad and pose a security
risk, especially for pull requests from forks.
4. **Run jobs in parallel** -- Structure independent jobs (lint, test, typecheck)
to run concurrently. Use `needs` only when there is a real dependency between
jobs.
5. **Use `fail-fast: false` in matrix builds** -- By default a failing matrix
combination cancels all others. Setting `fail-fast: false` lets all
combinations complete so you get the full picture of what is broken.
6. **Use environment protection rules** -- Configure required reviewers and wait
timers on production environments in GitHub Settings. This adds a human gate
before production deploys.
7. **Extract reusable workflows and composite actions** -- If the same steps
appear in multiple workflows, factor them into a reusable workflow
(`workflow_call`) or composite action to keep things DRY.
8. **Keep secrets out of logs** -- Never `echo` a secret. GitHub masks known
secrets, but dynamically constructed values may leak. Use `::add-mask::` for
runtime values that should be hidden.
---
## Common Pitfalls
- **Slow pipelines**: Add caching
- **Secret exposure**: Never echo secrets
- **Unpinned versions**: Use @v4 not @main
1. **Unpinned action versions** -- Using `actions/checkout@main` means your
workflow pulls whatever is on main today. A bad push to that action
repository could break or compromise your builds. Pin to a tag (`@v4`) or
SHA.
2. **Missing caching** -- Running `npm ci` or `pip install` from scratch on
every run wastes minutes. Always configure caching for your package manager,
or use the built-in `cache` option in setup actions (e.g.,
`actions/setup-node` has a `cache` input).
3. **Overly broad triggers** -- Triggering on every push to every branch floods
the queue. Restrict triggers to `main` and pull requests. Use `paths` or
`paths-ignore` to skip runs when only docs or unrelated files change.
4. **Secret exposure in pull requests from forks** -- Secrets are NOT available
in workflows triggered by `pull_request` from forks (by design). If your
workflow needs secrets for fork PRs, use `pull_request_target` carefully and
never check out untrusted code in that context.
5. **Large artifacts without retention limits** -- Uploading artifacts without
setting `retention-days` uses the repository default (90 days), consuming
storage quota. Set short retention for transient artifacts like test results
and coverage reports.
6. **Ignoring `if: always()` for cleanup** -- Steps after a failure are skipped
by default. If you need to upload test results, send notifications, or run
cleanup regardless of prior step results, use `if: always()` or
`if: failure()`.
---
## 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
@@ -0,0 +1,250 @@
# 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 |
@@ -0,0 +1,176 @@
# =============================================================================
# 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
@@ -0,0 +1,164 @@
# =============================================================================
# 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 }}
+662 -38
View File
@@ -1,91 +1,715 @@
---
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.
---
# Django
## Description
Django web framework with ORM, views, and REST framework patterns.
## When to Use
- Python web applications
- Admin interfaces
- Django REST Framework APIs
- Content-heavy sites with ORM-driven data models
## When NOT to Use
- FastAPI projects — use the `frameworks/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
---
## Core Patterns
### Models
### 1. Models & ORM
#### Field types and relationships
```python
from django.db import models
from django.utils import timezone
class Organization(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["name"]
def __str__(self):
return self.name
class User(models.Model):
class Role(models.TextChoices):
ADMIN = "admin", "Administrator"
MEMBER = "member", "Member"
VIEWER = "viewer", "Viewer"
email = models.EmailField(unique=True)
name = models.CharField(max_length=100)
organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
related_name="members",
)
role = models.CharField(max_length=20, choices=Role.choices, default=Role.MEMBER)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
ordering = ["-created_at"]
indexes = [
models.Index(fields=["email"]),
models.Index(fields=["organization", "role"]),
]
constraints = [
models.UniqueConstraint(
fields=["organization", "email"],
name="unique_org_email",
),
]
def __str__(self):
return self.email
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
class Project(models.Model):
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="owned_projects")
organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag, blank=True, related_name="projects")
# OneToOneField for 1:1 relationships
settings = models.OneToOneField(
"ProjectSettings", on_delete=models.CASCADE, null=True, blank=True
)
class ProjectSettings(models.Model):
is_public = models.BooleanField(default=False)
max_members = models.IntegerField(default=10)
```
### Views (Class-based)
#### Custom managers and QuerySet methods
```python
from django.views.generic import ListView, DetailView
class ActiveManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_active=True)
class UserListView(ListView):
model = User
template_name = 'users/list.html'
context_object_name = 'users'
class UserQuerySet(models.QuerySet):
def admins(self):
return self.filter(role=User.Role.ADMIN)
def in_organization(self, org_id):
return self.filter(organization_id=org_id)
def with_project_count(self):
return self.annotate(project_count=models.Count("owned_projects"))
class User(models.Model):
# ... fields ...
objects = UserQuerySet.as_manager()
active = ActiveManager()
```
#### F objects, Q objects, and annotations
```python
from django.db.models import F, Q, Count, Avg, Sum, Value, When, Case
# F objects: reference model fields in queries
Project.objects.filter(updated_at__gt=F("created_at"))
User.objects.update(login_count=F("login_count") + 1) # Atomic increment
# Q objects: complex lookups with OR, AND, NOT
User.objects.filter(
Q(role="admin") | Q(role="member"),
~Q(is_active=False), # NOT inactive
)
# Annotations and aggregations
orgs = Organization.objects.annotate(
member_count=Count("members"),
admin_count=Count("members", filter=Q(members__role="admin")),
avg_projects=Avg("members__owned_projects"),
).filter(member_count__gte=5)
# Conditional expressions
users = User.objects.annotate(
tier=Case(
When(owned_projects__count__gte=10, then=Value("power")),
When(owned_projects__count__gte=3, then=Value("active")),
default=Value("starter"),
)
)
# Subqueries
from django.db.models import Subquery, OuterRef
latest_project = Project.objects.filter(
owner=OuterRef("pk")
).order_by("-created_at").values("title")[:1]
users = User.objects.annotate(latest_project_title=Subquery(latest_project))
```
### 2. Views
#### Function-based views
```python
from django.shortcuts import render, get_object_or_404, redirect
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
@login_required
def project_detail(request, project_id):
project = get_object_or_404(
Project.objects.select_related("owner", "organization"),
pk=project_id,
)
if request.method == "POST":
form = ProjectForm(request.POST, instance=project)
if form.is_valid():
form.save()
return redirect("project-detail", project_id=project.id)
else:
form = ProjectForm(instance=project)
return render(request, "projects/detail.html", {
"project": project,
"form": form,
})
```
#### Class-based views
```python
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.urls import reverse_lazy
class ProjectListView(LoginRequiredMixin, ListView):
model = Project
template_name = "projects/list.html"
context_object_name = "projects"
paginate_by = 20
class UserDetailView(DetailView):
model = User
template_name = 'users/detail.html'
def get_queryset(self):
qs = super().get_queryset().select_related("owner", "organization")
search = self.request.GET.get("q")
if search:
qs = qs.filter(
Q(title__icontains=search) | Q(description__icontains=search)
)
return qs
class ProjectCreateView(LoginRequiredMixin, CreateView):
model = Project
form_class = ProjectForm
template_name = "projects/form.html"
success_url = reverse_lazy("project-list")
def form_valid(self, form):
form.instance.owner = self.request.user
form.instance.organization = self.request.user.organization
return super().form_valid(form)
class ProjectDeleteView(PermissionRequiredMixin, DeleteView):
model = Project
permission_required = "projects.delete_project"
success_url = reverse_lazy("project-list")
```
### Django REST Framework
#### Mixins for reuse
```python
from rest_framework import serializers, viewsets
class OrganizationFilterMixin:
"""Filter queryset to the current user's organization."""
def get_queryset(self):
return super().get_queryset().filter(
organization=self.request.user.organization
)
class UserSerializer(serializers.ModelSerializer):
class ProjectListView(LoginRequiredMixin, OrganizationFilterMixin, ListView):
model = Project
# queryset is automatically filtered by organization
```
#### API views with Django REST Framework
```python
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
@api_view(["GET", "POST"])
@permission_classes([IsAuthenticated])
def project_list(request):
if request.method == "GET":
projects = Project.objects.filter(organization=request.user.organization)
serializer = ProjectSerializer(projects, many=True)
return Response(serializer.data)
serializer = ProjectCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(owner=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
```
### 3. Migrations
#### Creating and running migrations
```bash
# Generate migrations after model changes
python manage.py makemigrations app_name
# Preview SQL without applying
python manage.py sqlmigrate app_name 0001
# Apply migrations
python manage.py migrate
# Show migration status
python manage.py showmigrations
```
#### Data migrations with RunPython
```python
from django.db import migrations
def populate_slugs(apps, schema_editor):
Organization = apps.get_model("myapp", "Organization")
from django.utils.text import slugify
for org in Organization.objects.filter(slug=""):
org.slug = slugify(org.name)
org.save(update_fields=["slug"])
def reverse_populate_slugs(apps, schema_editor):
pass # No-op reverse
class Migration(migrations.Migration):
dependencies = [
("myapp", "0005_add_slug_field"),
]
operations = [
migrations.RunPython(populate_slugs, reverse_populate_slugs),
]
```
#### Squashing migrations
```bash
# Squash migrations 0001 through 0010 into one
python manage.py squashmigrations app_name 0001 0010
```
**Tips:**
- Always provide a reverse function for `RunPython` (even if it is a no-op)
- Use `apps.get_model()` in data migrations, never import models directly
- Test migrations on a copy of production data before deploying
### 4. Forms
#### ModelForm with custom validation
```python
from django import forms
from django.core.exceptions import ValidationError
class ProjectForm(forms.ModelForm):
class Meta:
model = User
fields = ['id', 'email', 'name', 'created_at']
model = Project
fields = ["title", "description", "tags"]
widgets = {
"description": forms.Textarea(attrs={"rows": 4}),
"tags": forms.CheckboxSelectMultiple(),
}
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
def clean_title(self):
title = self.cleaned_data["title"]
if "test" in title.lower() and not self.instance.pk:
raise ValidationError("Title cannot contain 'test' for new projects.")
return title
def clean(self):
cleaned = super().clean()
title = cleaned.get("title", "")
description = cleaned.get("description", "")
if len(title) + len(description) < 20:
raise ValidationError("Title + description must be at least 20 characters.")
return cleaned
```
### URLs
#### Formsets
```python
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from django.forms import inlineformset_factory
router = DefaultRouter()
router.register('users', UserViewSet)
TaskFormSet = inlineformset_factory(
Project,
Task,
fields=["title", "assigned_to", "due_date"],
extra=2, # Number of empty forms
can_delete=True,
max_num=20,
)
urlpatterns = [
path('api/', include(router.urls)),
# In a view
def project_tasks(request, project_id):
project = get_object_or_404(Project, pk=project_id)
if request.method == "POST":
formset = TaskFormSet(request.POST, instance=project)
if formset.is_valid():
formset.save()
return redirect("project-detail", project_id=project.id)
else:
formset = TaskFormSet(instance=project)
return render(request, "projects/tasks.html", {"formset": formset})
```
### 5. Signals
```python
from django.db.models.signals import post_save, pre_save, m2m_changed
from django.dispatch import receiver
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
@receiver(pre_save, sender=Project)
def set_project_slug(sender, instance, **kwargs):
if not instance.slug:
from django.utils.text import slugify
instance.slug = slugify(instance.title)
# Custom signals
from django.dispatch import Signal
project_published = Signal() # Accepts sender
@receiver(project_published)
def notify_members(sender, project, **kwargs):
for member in project.organization.members.all():
send_notification(member, f"Project '{project.title}' published")
# Firing a custom signal
project_published.send(sender=Project, project=project)
```
**When to use signals vs overriding `save()`:**
- Use signals when the action is a side effect (notifications, logging, cache invalidation)
- Override `save()` when the logic is core to the model's behavior (setting computed fields)
### 6. Middleware
```python
import time
from django.utils.deprecation import MiddlewareMixin
class TimingMiddleware(MiddlewareMixin):
def process_request(self, request):
request._start_time = time.perf_counter()
def process_response(self, request, response):
if hasattr(request, "_start_time"):
duration = time.perf_counter() - request._start_time
response["X-Process-Time"] = f"{duration:.4f}"
return response
# New-style middleware (function-based)
def organization_middleware(get_response):
def middleware(request):
if request.user.is_authenticated:
request.organization = request.user.organization
else:
request.organization = None
response = get_response(request)
return response
return middleware
# Register in settings.py
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"myapp.middleware.organization_middleware", # Custom
"myapp.middleware.TimingMiddleware", # Custom
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
```
### 7. Django REST Framework
#### Serializers
```python
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
project_count = serializers.IntegerField(read_only=True)
full_name = serializers.SerializerMethodField()
class Meta:
model = User
fields = ["id", "email", "name", "role", "full_name", "project_count", "created_at"]
read_only_fields = ["id", "created_at"]
def get_full_name(self, obj):
return f"{obj.name} ({obj.role})"
class ProjectSerializer(serializers.ModelSerializer):
owner = UserSerializer(read_only=True)
tags = serializers.SlugRelatedField(
many=True, slug_field="name", queryset=Tag.objects.all()
)
class Meta:
model = Project
fields = ["id", "title", "description", "owner", "tags", "created_at"]
def validate_title(self, value):
if len(value) < 3:
raise serializers.ValidationError("Title must be at least 3 characters.")
return value
class ProjectCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ["title", "description", "tags"]
```
#### ViewSets and routers
```python
from rest_framework import viewsets, permissions, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
class ProjectViewSet(viewsets.ModelViewSet):
serializer_class = ProjectSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ["owner", "tags"]
search_fields = ["title", "description"]
ordering_fields = ["created_at", "title"]
ordering = ["-created_at"]
def get_queryset(self):
return Project.objects.filter(
organization=self.request.user.organization
).select_related("owner").prefetch_related("tags")
def get_serializer_class(self):
if self.action == "create":
return ProjectCreateSerializer
return ProjectSerializer
def perform_create(self, serializer):
serializer.save(
owner=self.request.user,
organization=self.request.user.organization,
)
@action(detail=True, methods=["post"])
def publish(self, request, pk=None):
project = self.get_object()
project.is_published = True
project.save(update_fields=["is_published"])
return Response({"status": "published"})
# urls.py
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register("projects", ProjectViewSet, basename="project")
router.register("users", UserViewSet, basename="user")
urlpatterns = [
path("api/", include(router.urls)),
]
```
#### Permissions
```python
from rest_framework.permissions import BasePermission
class IsOrganizationAdmin(BasePermission):
def has_permission(self, request, view):
return (
request.user.is_authenticated
and request.user.role == User.Role.ADMIN
)
class IsOwnerOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.owner == request.user
```
#### Pagination
```python
from rest_framework.pagination import PageNumberPagination, CursorPagination
class StandardPagination(PageNumberPagination):
page_size = 20
page_size_query_param = "page_size"
max_page_size = 100
class TimelinePagination(CursorPagination):
page_size = 50
ordering = "-created_at"
# settings.py
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "myapp.pagination.StandardPagination",
"PAGE_SIZE": 20,
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
}
```
### 8. Admin
```python
from django.contrib import admin
from django.utils.html import format_html
class TaskInline(admin.TabularInline):
model = Task
extra = 0
fields = ["title", "assigned_to", "status", "due_date"]
readonly_fields = ["created_at"]
@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
list_display = ["title", "owner_name", "organization", "tag_list", "created_at"]
list_filter = ["organization", "tags", "created_at"]
search_fields = ["title", "description", "owner__email"]
readonly_fields = ["created_at", "updated_at"]
autocomplete_fields = ["owner", "organization"]
prepopulated_fields = {"slug": ("title",)}
date_hierarchy = "created_at"
inlines = [TaskInline]
fieldsets = (
(None, {
"fields": ("title", "slug", "description"),
}),
("Ownership", {
"fields": ("owner", "organization", "tags"),
}),
("Metadata", {
"classes": ("collapse",),
"fields": ("created_at", "updated_at"),
}),
)
def owner_name(self, obj):
return obj.owner.name
owner_name.short_description = "Owner"
owner_name.admin_order_field = "owner__name"
def tag_list(self, obj):
return ", ".join(t.name for t in obj.tags.all())
tag_list.short_description = "Tags"
def get_queryset(self, request):
return super().get_queryset(request).select_related(
"owner", "organization"
).prefetch_related("tags")
# Custom admin actions
@admin.action(description="Mark selected projects as published")
def make_published(self, request, queryset):
count = queryset.update(is_published=True)
self.message_user(request, f"{count} projects published.")
actions = [make_published]
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
list_display = ["email", "name", "organization", "role", "is_active"]
list_filter = ["role", "is_active", "organization"]
search_fields = ["email", "name"]
list_editable = ["role", "is_active"]
list_per_page = 50
```
---
## Best Practices
1. Use class-based views for standard CRUD
2. Define model methods for business logic
3. Use serializers for validation
4. Add proper permissions
5. Use select_related/prefetch_related for queries
1. **Use `select_related` and `prefetch_related` on every query that touches relations**`select_related` for ForeignKey/OneToOne (SQL JOIN), `prefetch_related` for ManyToMany and reverse ForeignKey (separate query). Check queries with `django-debug-toolbar`.
2. **Keep business logic in model methods or service functions, not in views** — views should handle HTTP, forms should handle validation, models/services should handle domain logic. This makes code testable without needing HTTP.
3. **Use `get_queryset()` for dynamic filtering instead of hardcoding querysets** — both in views and DRF ViewSets. This enables mixin composition and per-request filtering (e.g., by organization).
4. **Write data migrations for schema changes that require backfills** — never assume fields can be added as non-nullable without a migration to populate existing rows. Use `RunPython` with a reverse function.
5. **Configure Django REST Framework defaults in settings** — set `DEFAULT_PAGINATION_CLASS`, `DEFAULT_PERMISSION_CLASSES`, `DEFAULT_AUTHENTICATION_CLASSES` in `REST_FRAMEWORK` dict to avoid repeating yourself on each ViewSet.
6. **Use `TextChoices` / `IntegerChoices` for enum fields** — they integrate with admin filters, serializer validation, and migrations automatically. Avoid plain strings or integers for status/role fields.
7. **Index frequently queried fields** — add `db_index=True` on individual fields or use `Meta.indexes` for composite indexes. Add `UniqueConstraint` for business-rule uniqueness.
8. **Use Django's `transaction.atomic()` for multi-step writes** — wrap create/update sequences that must succeed or fail together. DRF's `perform_create` and `perform_update` are good places for this.
```python
from django.db import transaction
@transaction.atomic
def transfer_project(project, new_owner):
old_owner = project.owner
project.owner = new_owner
project.save(update_fields=["owner"])
AuditLog.objects.create(
action="transfer",
project=project,
from_user=old_owner,
to_user=new_owner,
)
```
---
## Common Pitfalls
- **N+1 queries**: Use select_related/prefetch_related
- **Missing migrations**: Run makemigrations
- **No validation**: Use serializers properly
1. **N+1 queries** — accessing `project.owner.name` in a loop without `select_related("owner")` fires one query per iteration. Use `django-debug-toolbar` or `nplusone` to detect these. Always optimize queryset in `get_queryset()`.
2. **Importing models directly in data migrations** — models change over time, but migrations are frozen. Always use `apps.get_model("app_name", "ModelName")` inside `RunPython` functions, never `from myapp.models import Model`.
3. **Forgetting to call `full_clean()` in model saves** — Django's `save()` does NOT run validators by default. Only forms and serializers call `full_clean()`. If you save models directly, add explicit validation.
4. **Circular imports between apps** — referencing models across apps can cause import cycles. Use string references in ForeignKey: `models.ForeignKey("other_app.ModelName", ...)` instead of importing the class.
5. **Overusing signals for core logic** — signals make code harder to trace and debug. Use them for side effects (sending emails, cache invalidation), not for core domain logic. If logic should always run on save, override `save()` instead.
6. **Returning entire QuerySets from service functions** — QuerySets are lazy, which is usually good, but returning them from service layers can lead to unexpected queries executing in templates. Use `.values()`, `.values_list()`, or serialize to dicts when crossing layer boundaries.
---
## 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
@@ -0,0 +1,250 @@
# 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"]
```
+638 -47
View File
@@ -1,89 +1,680 @@
---
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.
---
# FastAPI
## Description
FastAPI web framework with async patterns, Pydantic validation, and OpenAPI documentation.
## When to Use
- Building REST APIs with Python
- Async web applications
- OpenAPI/Swagger documentation needed
- Python microservices
- WebSocket real-time applications
## When NOT to Use
- Django projects — use the `frameworks/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
---
## Core Patterns
### Route Definition
### 1. Project Structure
Recommended layout for medium-large FastAPI applications:
```
project/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI app creation, startup/shutdown
│ ├── config.py # Settings via pydantic-settings
│ ├── dependencies.py # Shared dependencies
│ ├── exceptions.py # Custom exception handlers
│ ├── middleware.py # Custom middleware
│ ├── api/
│ │ ├── __init__.py
│ │ ├── router.py # Root router aggregating all sub-routers
│ │ ├── v1/
│ │ │ ├── __init__.py
│ │ │ ├── users.py # /api/v1/users endpoints
│ │ │ ├── items.py # /api/v1/items endpoints
│ │ │ └── auth.py # /api/v1/auth endpoints
│ │ └── v2/ # Future API version
│ ├── models/
│ │ ├── __init__.py
│ │ ├── user.py # SQLAlchemy / SQLModel ORM models
│ │ └── item.py
│ ├── schemas/
│ │ ├── __init__.py
│ │ ├── user.py # Pydantic request/response schemas
│ │ └── item.py
│ ├── services/
│ │ ├── __init__.py
│ │ ├── user_service.py # Business logic layer
│ │ └── item_service.py
│ ├── repositories/
│ │ ├── __init__.py
│ │ ├── user_repo.py # Data access layer
│ │ └── item_repo.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── database.py # DB engine, session factory
│ │ └── security.py # JWT, hashing, auth utils
│ └── tests/
│ ├── __init__.py
│ ├── conftest.py # Fixtures: test client, test DB
│ ├── test_users.py
│ └── test_items.py
├── alembic/ # Database migrations
│ ├── env.py
│ └── versions/
├── alembic.ini
├── pyproject.toml
├── Dockerfile
└── .env
```
**Key conventions:**
- Separate `schemas/` (Pydantic) from `models/` (ORM) to keep concerns clean
- Use `services/` for business logic, `repositories/` for data access
- Version API routes under `api/v1/`, `api/v2/` for backward compatibility
- Keep `main.py` thin — it only wires things together
```python
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel
# app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.api.router import api_router
from app.config import settings
from app.core.database import engine
from app.middleware import add_middleware
app = FastAPI()
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: create tables, warm caches, connect to services
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
# Shutdown: close connections, flush buffers
await engine.dispose()
class UserCreate(BaseModel):
email: str
name: str
app = FastAPI(
title=settings.PROJECT_NAME,
version=settings.VERSION,
lifespan=lifespan,
)
add_middleware(app)
app.include_router(api_router, prefix="/api")
```
class UserResponse(BaseModel):
id: int
email: str
name: str
### 2. Route Patterns
@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate):
# Create user logic
return UserResponse(id=1, **user.model_dump())
#### APIRouter with tags, prefixes, and dependencies
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
user = await get_user_by_id(user_id)
```python
from fastapi import APIRouter, Depends, Query, Path, Body, HTTPException, status
from app.schemas.user import UserCreate, UserResponse, UserUpdate, UserList
from app.dependencies import get_current_user
router = APIRouter(
prefix="/users",
tags=["users"],
dependencies=[Depends(get_current_user)], # Applied to all routes
responses={401: {"description": "Not authenticated"}},
)
# Path parameters with validation
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
user_id: int = Path(..., gt=0, description="The ID of the user to retrieve"),
):
user = await user_service.get(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
# Query parameters with defaults and validation
@router.get("/", response_model=UserList)
async def list_users(
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(20, ge=1, le=100, description="Max records to return"),
search: str | None = Query(None, min_length=1, max_length=100),
sort_by: str = Query("created_at", pattern="^(created_at|name|email)$"),
):
users = await user_service.list(skip=skip, limit=limit, search=search)
return users
# Request body with status codes
@router.post(
"/",
response_model=UserResponse,
status_code=status.HTTP_201_CREATED,
summary="Create a new user",
description="Creates a user account and sends a welcome email.",
)
async def create_user(user: UserCreate = Body(...)):
return await user_service.create(user)
# Multiple response models for different status codes
@router.put("/{user_id}", response_model=UserResponse, responses={
404: {"description": "User not found"},
409: {"description": "Email already taken"},
})
async def update_user(user_id: int, user: UserUpdate):
return await user_service.update(user_id, user)
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(user_id: int):
await user_service.delete(user_id)
```
### Dependency Injection
#### Router aggregation
```python
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
# app/api/router.py
from fastapi import APIRouter
from app.api.v1 import users, items, auth
api_router = APIRouter()
api_router.include_router(auth.router, prefix="/v1")
api_router.include_router(users.router, prefix="/v1")
api_router.include_router(items.router, prefix="/v1")
```
### 3. Dependency Injection
#### Basic dependency with Depends()
```python
from fastapi import Depends, Header, HTTPException
async def verify_api_key(x_api_key: str = Header(...)):
if x_api_key != settings.API_KEY:
raise HTTPException(status_code=403, detail="Invalid API key")
return x_api_key
```
#### Nested dependencies
```python
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session_maker() as session:
yield session
yield session # yield dependency — cleanup runs after response
@app.get("/users")
async def list_users(db: AsyncSession = Depends(get_db)):
return await db.execute(select(User))
async def get_user_repo(db: AsyncSession = Depends(get_db)) -> UserRepository:
return UserRepository(db)
async def get_user_service(
repo: UserRepository = Depends(get_user_repo),
) -> UserService:
return UserService(repo)
@router.get("/users")
async def list_users(service: UserService = Depends(get_user_service)):
return await service.list_all()
```
### Router Organization
#### Yield dependencies for cleanup
```python
from fastapi import APIRouter
async def get_redis() -> AsyncGenerator[Redis, None]:
redis = await aioredis.from_url(settings.REDIS_URL)
try:
yield redis
finally:
await redis.close() # Always runs, even on exceptions
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/")
async def list_users():
pass
# In main.py
app.include_router(router)
async def get_http_client() -> AsyncGenerator[httpx.AsyncClient, None]:
async with httpx.AsyncClient(timeout=30.0) as client:
yield client
```
#### Request-scoped dependencies with caching
```python
# FastAPI caches dependency results per-request by default.
# The same db session is reused if multiple deps request it.
@router.get("/dashboard")
async def dashboard(
user_service: UserService = Depends(get_user_service),
item_service: ItemService = Depends(get_item_service),
# Both services share the same db session from get_db()
):
users = await user_service.count()
items = await item_service.count()
return {"users": users, "items": items}
# To disable caching (get fresh instance each time):
@router.get("/example")
async def example(
db1: AsyncSession = Depends(get_db),
db2: AsyncSession = Depends(get_db, use_cache=False),
# db1 and db2 are different sessions
):
pass
```
#### Class-based dependencies
```python
class Pagination:
def __init__(
self,
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
):
self.skip = skip
self.limit = limit
@router.get("/items")
async def list_items(pagination: Pagination = Depends()):
# pagination.skip, pagination.limit
pass
```
### 4. Middleware
#### Custom middleware
```python
import time
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
response.headers["X-Process-Time"] = f"{duration:.4f}"
return response
```
#### Pure ASGI middleware (higher performance)
```python
from starlette.types import ASGIApp, Receive, Scope, Send
class RequestIDMiddleware:
def __init__(self, app: ASGIApp):
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send):
if scope["type"] == "http":
request_id = uuid.uuid4().hex
scope.setdefault("state", {})["request_id"] = request_id
async def send_with_header(message):
if message["type"] == "http.response.start":
headers = dict(message.get("headers", []))
headers[b"x-request-id"] = request_id.encode()
message["headers"] = list(headers.items())
await send(message)
await self.app(scope, receive, send_with_header)
else:
await self.app(scope, receive, send)
```
#### Standard middleware configuration
```python
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.middleware.gzip import GZipMiddleware
def add_middleware(app: FastAPI):
# Order matters: first added = outermost (runs first on request, last on response)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS, # ["https://example.com"]
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
allow_headers=["*"],
)
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=settings.ALLOWED_HOSTS, # ["example.com", "*.example.com"]
)
app.add_middleware(GZipMiddleware, minimum_size=500) # Compress responses > 500 bytes
app.add_middleware(TimingMiddleware)
```
### 5. Background Tasks
#### Simple background tasks
```python
from fastapi import BackgroundTasks
async def send_welcome_email(email: str, name: str):
# This runs after the response is sent
await email_service.send(
to=email,
subject="Welcome!",
body=f"Hello {name}, welcome to our platform.",
)
async def log_activity(user_id: int, action: str):
await activity_repo.create(user_id=user_id, action=action)
@router.post("/users", status_code=201)
async def create_user(
user: UserCreate,
background_tasks: BackgroundTasks,
):
new_user = await user_service.create(user)
# Queue multiple background tasks
background_tasks.add_task(send_welcome_email, new_user.email, new_user.name)
background_tasks.add_task(log_activity, new_user.id, "account_created")
return new_user
```
#### Long-running tasks with task queues
For tasks that take more than a few seconds, use a proper task queue:
```python
from celery import Celery
celery_app = Celery("worker", broker=settings.CELERY_BROKER_URL)
@celery_app.task
def generate_report(report_id: int):
# Long-running: query data, build PDF, upload to S3
...
@router.post("/reports", status_code=202)
async def request_report(params: ReportRequest):
report = await report_service.create(params)
generate_report.delay(report.id) # Dispatch to Celery worker
return {"report_id": report.id, "status": "processing"}
@router.get("/reports/{report_id}/status")
async def report_status(report_id: int):
report = await report_service.get(report_id)
return {"status": report.status, "url": report.download_url}
```
### 6. WebSocket
#### WebSocket endpoint with connection management
```python
from fastapi import WebSocket, WebSocketDisconnect
class ConnectionManager:
def __init__(self):
self.active_connections: dict[str, list[WebSocket]] = {}
async def connect(self, websocket: WebSocket, room: str):
await websocket.accept()
self.active_connections.setdefault(room, []).append(websocket)
def disconnect(self, websocket: WebSocket, room: str):
self.active_connections.get(room, []).remove(websocket)
async def broadcast(self, message: str, room: str):
for connection in self.active_connections.get(room, []):
try:
await connection.send_text(message)
except WebSocketDisconnect:
self.disconnect(connection, room)
manager = ConnectionManager()
@app.websocket("/ws/{room}")
async def websocket_endpoint(websocket: WebSocket, room: str):
await manager.connect(websocket, room)
try:
while True:
data = await websocket.receive_text()
await manager.broadcast(f"Message: {data}", room)
except WebSocketDisconnect:
manager.disconnect(websocket, room)
await manager.broadcast(f"User left the room", room)
```
#### WebSocket with authentication
```python
@app.websocket("/ws/private")
async def private_ws(websocket: WebSocket, token: str = Query(...)):
try:
user = verify_token(token)
except InvalidToken:
await websocket.close(code=4001, reason="Invalid token")
return
await websocket.accept()
try:
while True:
data = await websocket.receive_json()
response = await process_message(user, data)
await websocket.send_json(response)
except WebSocketDisconnect:
pass
```
### 7. File Handling
#### Upload files
```python
from fastapi import UploadFile, File
@router.post("/upload")
async def upload_file(
file: UploadFile = File(..., description="File to upload"),
):
# Validate file type and size
if file.content_type not in ["image/png", "image/jpeg"]:
raise HTTPException(400, "Only PNG and JPEG images are allowed")
if file.size and file.size > 5 * 1024 * 1024: # 5 MB
raise HTTPException(400, "File too large (max 5 MB)")
contents = await file.read()
path = f"uploads/{uuid.uuid4()}_{file.filename}"
async with aiofiles.open(path, "wb") as f:
await f.write(contents)
return {"filename": file.filename, "path": path, "size": len(contents)}
# Multiple file upload
@router.post("/upload-multiple")
async def upload_multiple(files: list[UploadFile] = File(...)):
results = []
for file in files:
contents = await file.read()
results.append({"filename": file.filename, "size": len(contents)})
return results
```
#### Streaming responses
```python
from fastapi.responses import StreamingResponse
import csv
import io
@router.get("/export/users")
async def export_users():
async def generate_csv():
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["id", "name", "email"])
yield output.getvalue()
output.seek(0)
output.truncate(0)
async for user in user_service.stream_all():
writer.writerow([user.id, user.name, user.email])
yield output.getvalue()
output.seek(0)
output.truncate(0)
return StreamingResponse(
generate_csv(),
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=users.csv"},
)
```
#### Static files
```python
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory="static"), name="static")
```
### 8. Testing
#### TestClient for synchronous tests
```python
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_create_user():
response = client.post("/api/v1/users", json={
"email": "test@example.com",
"name": "Test User",
})
assert response.status_code == 201
data = response.json()
assert data["email"] == "test@example.com"
def test_get_user_not_found():
response = client.get("/api/v1/users/99999")
assert response.status_code == 404
```
#### Async testing with httpx
```python
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
@pytest.fixture
async def async_client():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
yield client
@pytest.mark.anyio
async def test_list_users(async_client: AsyncClient):
response = await async_client.get("/api/v1/users")
assert response.status_code == 200
assert isinstance(response.json(), list)
```
#### Overriding dependencies for tests
```python
from app.dependencies import get_db, get_current_user
from app.models.user import User
# Mock database session
async def override_get_db():
async with test_session_maker() as session:
yield session
# Mock authenticated user
async def override_get_current_user():
return User(id=1, email="test@example.com", name="Test")
app.dependency_overrides[get_db] = override_get_db
app.dependency_overrides[get_current_user] = override_get_current_user
# In conftest.py — clean up after tests
@pytest.fixture(autouse=True)
def clear_overrides():
yield
app.dependency_overrides.clear()
```
#### Testing WebSocket endpoints
```python
def test_websocket():
with client.websocket_connect("/ws/test-room") as ws:
ws.send_text("hello")
data = ws.receive_text()
assert "hello" in data
```
---
## Best Practices
1. Use Pydantic models for request/response validation
2. Organize routes with APIRouter
3. Use dependency injection for services
4. Return proper HTTP status codes
5. Add OpenAPI descriptions
1. **Use Pydantic models for all request/response validation** — never pass raw dicts through your API boundary. Define separate `Create`, `Update`, and `Response` schemas for each resource.
2. **Organize routes with APIRouter** — group related endpoints by resource and version. Apply shared dependencies at the router level, not on each individual route.
3. **Separate business logic from routes** — route functions should only handle HTTP concerns (parsing request, returning response). Delegate logic to service classes injected via `Depends()`.
4. **Use the lifespan context manager** — replace deprecated `on_event("startup")` and `on_event("shutdown")` with the `lifespan` async context manager for resource setup and teardown.
5. **Return proper HTTP status codes** — 201 for creation, 204 for deletion, 202 for accepted-but-not-done, 409 for conflicts. Use `status_code` parameter on route decorators.
6. **Add OpenAPI metadata** — provide `summary`, `description`, `tags`, and `responses` on routes. Set `title`, `version`, and `description` on the FastAPI app. This generates high-quality auto-docs.
7. **Use async all the way down** — if your route is `async def`, every I/O call inside it must also be async. Mixing sync blocking calls (e.g., `requests.get()`) in an async route will block the event loop.
8. **Configure settings with pydantic-settings** — load config from environment variables with validation and type coercion. Never hardcode secrets or connection strings.
```python
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str
API_KEY: str
DEBUG: bool = False
model_config = {"env_file": ".env"}
settings = Settings()
```
---
## Common Pitfalls
- **Blocking I/O in async**: Use async libraries
- **Missing response models**: Always define them
- **No error handling**: Use HTTPException properly
1. **Blocking I/O in async routes** — calling `requests.get()`, `time.sleep()`, or synchronous DB drivers inside `async def` routes starves the event loop. Use `httpx`, `asyncio.sleep()`, and async database drivers instead. If you must call sync code, use `run_in_executor`.
2. **Missing response_model** — without `response_model`, FastAPI returns whatever you return, potentially leaking internal fields (passwords, internal IDs). Always define a Pydantic response schema.
3. **Forgetting to await coroutines** — calling `await db.execute(query)` vs `db.execute(query)` is easy to miss. The latter returns a coroutine object instead of results. Enable linting rules that catch unawaited coroutines.
4. **Circular imports between models and schemas** — when schemas reference ORM models and vice versa, you get import cycles. Fix by using `TYPE_CHECKING` imports or by keeping schemas and models in separate modules that do not import each other.
5. **Not handling Pydantic validation errors** — FastAPI returns 422 by default, but the error format may confuse API consumers. Add a custom exception handler to reshape validation error responses to match your API's error format.
6. **Sharing mutable state across requests without locks** — global mutable variables (lists, dicts) accessed from async routes can cause race conditions. Use async-safe structures or dependency-injected per-request state.
---
## 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
@@ -0,0 +1,229 @@
# 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)): ...
```
+639 -59
View File
@@ -1,112 +1,692 @@
---
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.
---
# Next.js
## Description
Next.js framework with App Router, Server Components, and full-stack development patterns.
## When to Use
- React applications with SSR/SSG
- Full-stack applications
- App Router patterns
- SEO-critical sites needing server rendering
## When NOT to Use
- Pure React SPAs without SSR needs — use the `frameworks/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`
---
## Core Patterns
### App Router Structure
### 1. App Router
#### Directory structure
```
app/
├── layout.tsx # Root layout
├── page.tsx # Home page
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── layout.tsx # Root layout (wraps entire app)
├── page.tsx # Home page (/)
├── loading.tsx # Root loading UI (Suspense fallback)
├── error.tsx # Root error boundary
├── not-found.tsx # Custom 404 page
├── global-error.tsx # Error boundary for root layout itself
├── favicon.ico
├── globals.css
├── api/
── users/
└── route.ts # API route
└── users/
── page.tsx # Users page
└── [id]/
└── page.tsx # User detail
── users/
└── route.ts # GET/POST /api/users
│ │ └── [id]/
│ │ ── route.ts # GET/PUT/DELETE /api/users/:id
└── webhooks/
└── stripe/
│ └── route.ts # POST /api/webhooks/stripe
├── (marketing)/ # Route group (no URL segment)
│ ├── layout.tsx # Layout for marketing pages only
│ ├── page.tsx # / (same as root, can override)
│ ├── about/
│ │ └── page.tsx # /about
│ └── pricing/
│ └── page.tsx # /pricing
├── (app)/ # Route group for authenticated app
│ ├── layout.tsx # App shell layout (sidebar, nav)
│ ├── dashboard/
│ │ ├── page.tsx # /dashboard
│ │ ├── loading.tsx # Loading skeleton for dashboard
│ │ └── error.tsx # Error boundary for dashboard
│ ├── projects/
│ │ ├── page.tsx # /projects
│ │ └── [id]/
│ │ ├── page.tsx # /projects/:id
│ │ ├── edit/
│ │ │ └── page.tsx # /projects/:id/edit
│ │ └── layout.tsx # Shared layout for project detail
│ └── settings/
│ └── page.tsx # /settings
└── @modal/ # Parallel route slot
└── (.)projects/
└── [id]/
└── page.tsx # Intercepted route modal
```
### Server Components
#### Special files and their roles
| File | Purpose | Renders when |
|------|---------|-------------|
| `page.tsx` | Route UI | URL matches segment |
| `layout.tsx` | Shared wrapper, preserved across navigation | Always for child routes |
| `loading.tsx` | Suspense fallback | While page/data is loading |
| `error.tsx` | Error boundary | When child throws |
| `not-found.tsx` | 404 UI | When `notFound()` is called |
| `route.ts` | API endpoint | HTTP request to segment |
| `template.tsx` | Like layout but re-mounts on navigation | Every navigation |
| `default.tsx` | Fallback for parallel routes | When slot has no match |
```tsx
// app/users/page.tsx - Server Component (default)
async function UsersPage() {
const users = await db.users.findMany();
// app/layout.tsx — Root layout (required)
import type { Metadata } from "next";
export const metadata: Metadata = {
title: { default: "My App", template: "%s | My App" },
description: "Application description",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<nav>{/* Global navigation */}</nav>
<main>{children}</main>
</body>
</html>
);
}
// app/error.tsx — Error boundary (must be client component)
"use client";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>Page not found</h2>
<p>The requested resource does not exist.</p>
</div>
);
}
```
### 2. Server vs Client Components
#### Decision guide
| Use Server Component when | Use Client Component when |
|---------------------------|--------------------------|
| Fetching data | Using useState, useEffect, useRef |
| Accessing backend resources directly | Adding event handlers (onClick, onChange) |
| Keeping sensitive data on server | Using browser APIs (localStorage, window) |
| Reducing client bundle size | Using third-party client libraries |
| SEO-critical content | Animations, real-time updates |
#### Composition patterns
```tsx
// Server Component (default — no directive needed)
// app/projects/page.tsx
import { ProjectList } from "./project-list";
import { SearchBar } from "./search-bar"; // Client component
export default async function ProjectsPage() {
const projects = await db.project.findMany({
orderBy: { createdAt: "desc" },
});
return (
<div>
<h1>Projects</h1>
{/* Client component receives server data as props */}
<SearchBar />
{/* Server component can render client children */}
<ProjectList projects={projects} />
</div>
);
}
// Client Component — must have "use client" at top
// app/projects/search-bar.tsx
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useTransition } from "react";
export function SearchBar() {
const router = useRouter();
const searchParams = useSearchParams();
const [isPending, startTransition] = useTransition();
function handleSearch(term: string) {
const params = new URLSearchParams(searchParams);
if (term) {
params.set("q", term);
} else {
params.delete("q");
}
startTransition(() => {
router.replace(`/projects?${params.toString()}`);
});
}
return (
<input
type="search"
placeholder="Search projects..."
defaultValue={searchParams.get("q") ?? ""}
onChange={(e) => handleSearch(e.target.value)}
className={isPending ? "opacity-50" : ""}
/>
);
}
```
**Key rule:** The `"use client"` directive creates a boundary. Everything imported into a client component becomes part of the client bundle. Pass server data down as serializable props (no functions, no classes).
### 3. Data Fetching
#### Server component fetch with caching
```tsx
// Fetch with automatic deduplication and caching
async function getProjects() {
const res = await fetch("https://api.example.com/projects", {
next: { revalidate: 60 }, // Revalidate every 60 seconds (ISR)
// next: { tags: ["projects"] }, // Tag-based revalidation
// cache: "no-store", // Always fresh (SSR)
// cache: "force-cache", // Cache indefinitely (SSG)
});
if (!res.ok) throw new Error("Failed to fetch projects");
return res.json();
}
export default async function ProjectsPage() {
const projects = await getProjects();
return <ProjectList projects={projects} />;
}
```
#### generateStaticParams for static generation
```tsx
// app/projects/[id]/page.tsx
export async function generateStaticParams() {
const projects = await db.project.findMany({ select: { id: true } });
return projects.map((p) => ({ id: String(p.id) }));
}
export default async function ProjectPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const project = await db.project.findUnique({ where: { id } });
if (!project) notFound();
return <ProjectDetail project={project} />;
}
```
#### Route handlers (API routes)
```typescript
// app/api/projects/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const page = Number(searchParams.get("page") ?? "1");
const limit = Number(searchParams.get("limit") ?? "20");
const projects = await db.project.findMany({
skip: (page - 1) * limit,
take: limit,
});
return NextResponse.json({ data: projects, page, limit });
}
export async function POST(request: NextRequest) {
const body = await request.json();
const project = await db.project.create({ data: body });
return NextResponse.json(project, { status: 201 });
}
// Dynamic route: app/api/projects/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const project = await db.project.findUnique({ where: { id } });
if (!project) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return NextResponse.json(project);
}
```
### 4. Server Actions
#### Form actions
```tsx
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { z } from "zod";
const ProjectSchema = z.object({
title: z.string().min(3).max(200),
description: z.string().optional(),
});
export async function createProject(prevState: unknown, formData: FormData) {
const parsed = ProjectSchema.safeParse({
title: formData.get("title"),
description: formData.get("description"),
});
if (!parsed.success) {
return { errors: parsed.error.flatten().fieldErrors };
}
const project = await db.project.create({ data: parsed.data });
revalidatePath("/projects");
redirect(`/projects/${project.id}`);
}
export async function deleteProject(id: string) {
await db.project.delete({ where: { id } });
revalidatePath("/projects");
}
```
#### Using actions in client components with useActionState
```tsx
"use client";
import { useActionState } from "react";
import { createProject } from "../actions";
export function CreateProjectForm() {
const [state, formAction, isPending] = useActionState(createProject, null);
return (
<form action={formAction}>
<input name="title" placeholder="Project title" required />
{state?.errors?.title && (
<p className="text-red-500">{state.errors.title[0]}</p>
)}
<textarea name="description" placeholder="Description" />
<button type="submit" disabled={isPending}>
{isPending ? "Creating..." : "Create Project"}
</button>
</form>
);
}
```
#### Optimistic updates
```tsx
"use client";
import { useOptimistic } from "react";
import { deleteProject } from "../actions";
export function ProjectList({ projects }: { projects: Project[] }) {
const [optimisticProjects, removeOptimistic] = useOptimistic(
projects,
(state, removedId: string) => state.filter((p) => p.id !== removedId),
);
async function handleDelete(id: string) {
removeOptimistic(id);
await deleteProject(id);
}
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
{optimisticProjects.map((project) => (
<li key={project.id}>
{project.title}
<button onClick={() => handleDelete(project.id)}>Delete</button>
</li>
))}
</ul>
);
}
```
### Client Components
### 5. Middleware
```typescript
// middleware.ts (root of project, NOT inside app/)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Auth check
const token = request.cookies.get("session")?.value;
if (pathname.startsWith("/dashboard") && !token) {
return NextResponse.redirect(new URL("/login", request.url));
}
// Add headers
const response = NextResponse.next();
response.headers.set("x-pathname", pathname);
// Geo-based redirect
const country = request.geo?.country;
if (pathname === "/" && country === "DE") {
return NextResponse.redirect(new URL("/de", request.url));
}
// Rewrite (URL stays same, content changes)
if (pathname.startsWith("/old-path")) {
return NextResponse.rewrite(new URL("/new-path", request.url));
}
return response;
}
// Matcher: only run middleware on specific paths
export const config = {
matcher: [
// Match all paths except static files and api routes
"/((?!_next/static|_next/image|favicon.ico|api).*)",
// Or match specific paths
// "/dashboard/:path*",
// "/projects/:path*",
],
};
```
### 6. Caching
#### Cache layers overview
| Layer | What it caches | Control |
|-------|---------------|---------|
| Request Memoization | `fetch()` calls with same URL during single render | Automatic, per-request |
| Data Cache | `fetch()` results across requests | `next: { revalidate }`, `cache` option |
| Full Route Cache | HTML and RSC payload of static routes | `export const dynamic = "force-dynamic"` |
| Router Cache | Client-side RSC payload | `router.refresh()`, time-based |
#### Revalidation strategies
```tsx
'use client';
// Time-based revalidation (ISR)
fetch(url, { next: { revalidate: 3600 } }); // 1 hour
import { useState } from 'react';
// On-demand revalidation by path
import { revalidatePath } from "next/cache";
revalidatePath("/projects"); // Revalidate specific page
revalidatePath("/projects", "layout"); // Revalidate layout and all pages under it
export function Counter() {
const [count, setCount] = useState(0);
// On-demand revalidation by tag
import { revalidateTag } from "next/cache";
// When fetching:
fetch(url, { next: { tags: ["projects"] } });
// When invalidating:
revalidateTag("projects");
// Route segment config
export const dynamic = "force-dynamic"; // Never cache (SSR)
export const revalidate = 60; // ISR with 60s interval
export const fetchCache = "default-cache";
```
#### unstable_cache for non-fetch data
```tsx
import { unstable_cache } from "next/cache";
const getCachedProjects = unstable_cache(
async (orgId: string) => {
return db.project.findMany({ where: { organizationId: orgId } });
},
["projects"], // Cache key parts
{ revalidate: 60, tags: ["projects"] },
);
export default async function ProjectsPage() {
const projects = await getCachedProjects("org-123");
return <ProjectList projects={projects} />;
}
```
### 7. Route Groups & Parallel Routes
#### Route groups with `(groupName)`
Route groups organize routes without affecting the URL:
```
app/
├── (marketing)/ # URL: / , /about, /pricing (no "marketing" in URL)
│ ├── layout.tsx # Marketing layout (hero, footer)
│ ├── page.tsx
│ └── about/page.tsx
├── (app)/ # URL: /dashboard, /projects
│ ├── layout.tsx # App layout (sidebar, auth)
│ └── dashboard/page.tsx
```
#### Parallel routes with `@slotName`
```
app/
├── layout.tsx
├── page.tsx
├── @analytics/
│ ├── page.tsx # Rendered in parallel
│ └── default.tsx # Fallback when no match
├── @sidebar/
│ ├── page.tsx
│ └── default.tsx
```
```tsx
// app/layout.tsx — receives parallel route slots as props
export default function Layout({
children,
analytics,
sidebar,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
sidebar: React.ReactNode;
}) {
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<div className="flex">
<aside>{sidebar}</aside>
<main>{children}</main>
<aside>{analytics}</aside>
</div>
);
}
```
### API Routes
#### Intercepting routes
```typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET() {
const users = await db.users.findMany();
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const data = await request.json();
const user = await db.users.create({ data });
return NextResponse.json(user, { status: 201 });
}
```
app/
├── projects/
│ ├── page.tsx # /projects — full list
│ └── [id]/
│ └── page.tsx # /projects/:id — full page
├── @modal/
│ ├── (.)projects/
│ │ └── [id]/
│ │ └── page.tsx # Intercepts /projects/:id as modal
│ └── default.tsx # No modal by default
```
### Server Actions
Convention: `(.)` = same level, `(..)` = one level up, `(...)` = root.
### 8. Image & Font Optimization
#### next/image
```tsx
// app/actions.ts
'use server';
import Image from "next/image";
export async function createUser(formData: FormData) {
const name = formData.get('name') as string;
await db.users.create({ data: { name } });
revalidatePath('/users');
// Local image (automatically gets width/height)
import heroImage from "@/public/hero.png";
export function Hero() {
return (
<Image
src={heroImage}
alt="Hero banner"
placeholder="blur" // Auto blur placeholder for local images
priority // Preload for LCP images
className="w-full h-auto"
/>
);
}
// Remote image (must specify dimensions)
export function Avatar({ url, name }: { url: string; name: string }) {
return (
<Image
src={url}
alt={name}
width={48}
height={48}
className="rounded-full"
/>
);
}
// next.config.ts — allow remote image domains
const config = {
images: {
remotePatterns: [
{ protocol: "https", hostname: "avatars.githubusercontent.com" },
{ protocol: "https", hostname: "**.cloudinary.com" },
],
},
};
```
#### next/font
```tsx
// app/layout.tsx
import { Inter, JetBrains_Mono } from "next/font/google";
import localFont from "next/font/local";
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-inter",
});
const mono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-mono",
});
const customFont = localFont({
src: "./fonts/CustomFont.woff2",
variable: "--font-custom",
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${mono.variable}`}>
<body className="font-sans">{children}</body>
</html>
);
}
```
```css
/* In Tailwind config or globals.css */
:root {
--font-sans: var(--font-inter);
--font-mono: var(--font-mono);
}
```
---
## Best Practices
1. Use Server Components by default
2. Add 'use client' only when needed
3. Colocate data fetching with components
4. Use loading.tsx for suspense boundaries
5. Implement proper error boundaries
1. **Default to Server Components** — only add `"use client"` when you need interactivity, event handlers, or browser APIs. Server Components reduce bundle size and allow direct data access.
2. **Colocate data fetching with the component that uses it** — fetch inside the Server Component that renders the data, not in a parent that passes it down. Next.js deduplicates identical `fetch()` calls automatically.
3. **Use `loading.tsx` for instant loading states** — every route segment can have a `loading.tsx` that wraps the page in a Suspense boundary. This gives users immediate feedback during navigation.
4. **Validate Server Action inputs** — Server Actions are public HTTP endpoints. Always validate with zod or similar. Never trust `formData` values without parsing and validating.
5. **Use route groups to share layouts without affecting URLs**`(marketing)` and `(app)` let you have completely different layouts (public vs authenticated) without nesting URL segments.
6. **Prefer `revalidatePath`/`revalidateTag` over `cache: "no-store"`** — on-demand revalidation gives you fresh data when it changes while still serving cached content for performance. Only use `"no-store"` for truly dynamic per-request data.
7. **Put middleware at the project root**`middleware.ts` must be at the same level as `app/`, not inside it. Use the `matcher` config to limit which paths it runs on for performance.
8. **Use `next/image` for all images** — it handles lazy loading, responsive sizes, format conversion (WebP/AVIF), and blur placeholders. Set `priority` on above-the-fold LCP images. Configure `remotePatterns` for external image sources.
---
## Common Pitfalls
- **Using hooks in Server Components**: Mark as 'use client'
- **Large client bundles**: Keep client components small
- **Missing loading states**: Add loading.tsx files
1. **Using hooks in Server Components**`useState`, `useEffect`, `useRouter` (from `next/navigation`) only work in Client Components. If you see "hooks can only be called inside a function component," add `"use client"` or restructure to push interactivity to a child component.
2. **Passing non-serializable props across the server/client boundary** — functions, class instances, and Dates cannot be passed from Server to Client Components. Serialize data to plain objects and strings before passing as props.
3. **Large client bundles from misplaced `"use client"`** — placing the directive too high in the tree pulls entire subtrees into the client bundle. Push `"use client"` as deep as possible, wrapping only the interactive leaf components.
4. **Stale data from aggressive caching** — the Full Route Cache and Data Cache can serve stale content. Use `revalidatePath()`/`revalidateTag()` in Server Actions and route handlers after mutations. Call `router.refresh()` on the client if needed.
5. **Missing `default.tsx` for parallel routes** — when navigating to a URL that does not match a parallel route slot, Next.js renders `default.tsx`. Without it, you get a 404. Always provide a default for every `@slot`.
6. **Forgetting `loading.tsx` leads to blank pages during navigation** — without loading boundaries, users see nothing while Server Components fetch data. Add `loading.tsx` at every route segment that does async work.
---
## 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
@@ -0,0 +1,237 @@
# 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 |
+679 -72
View File
@@ -1,108 +1,715 @@
---
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.
---
# React
## Description
React component patterns, hooks, and state management best practices.
## When to Use
- Building React components
- Using React hooks
- Component state management
- Client-side interactivity in any React-based framework
## When NOT to Use
- Vue, Svelte, or Angular projects — this skill is React-specific
- Backend-only projects without a frontend UI layer
- Static HTML pages that do not require a JavaScript framework
---
## Core Patterns
### Functional Components
### 1. Hooks
#### When-to-use guide
| Hook | Use when you need | Do NOT use for |
|------|-------------------|----------------|
| `useState` | Simple local state (toggle, form input, counter) | Derived/computed values |
| `useEffect` | Side effects: subscriptions, DOM mutations, timers | Data transformation (use useMemo) |
| `useRef` | Mutable value that persists across renders without triggering re-render; DOM refs | State that should cause re-render |
| `useMemo` | Expensive computation that should only rerun when deps change | Simple/cheap calculations |
| `useCallback` | Stable function reference to prevent child re-renders | Every function (only when needed) |
| `useReducer` | Complex state with multiple sub-values or state transitions | Simple boolean/string state |
| `useContext` | Reading context values | Frequently changing global state (causes re-renders) |
#### useState
```tsx
interface UserCardProps {
user: User;
onSelect?: (user: User) => void;
// Simple state
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
// Functional updates (when new state depends on previous)
setCount((prev) => prev + 1);
// Lazy initialization (expensive initial value)
const [data, setData] = useState(() => computeExpensiveDefault());
```
#### useEffect
```tsx
// Run on mount + cleanup on unmount
useEffect(() => {
const controller = new AbortController();
fetchData(controller.signal).then(setData);
return () => controller.abort(); // Cleanup
}, []); // Empty deps = run once
// Run when dependency changes
useEffect(() => {
const handler = () => setWidth(window.innerWidth);
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []); // No deps needed — handler is stable
// Sync external system with state
useEffect(() => {
document.title = `${count} items`;
}, [count]);
```
#### useRef
```tsx
// DOM reference
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => inputRef.current?.focus();
// Mutable value (no re-render on change)
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
// Previous value pattern
const prevValueRef = useRef(value);
useEffect(() => {
prevValueRef.current = value;
}, [value]);
```
#### useReducer
```tsx
interface State {
items: Item[];
loading: boolean;
error: string | null;
}
export function UserCard({ user, onSelect }: UserCardProps) {
type Action =
| { type: "FETCH_START" }
| { type: "FETCH_SUCCESS"; payload: Item[] }
| { type: "FETCH_ERROR"; error: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "FETCH_START":
return { ...state, loading: true, error: null };
case "FETCH_SUCCESS":
return { items: action.payload, loading: false, error: null };
case "FETCH_ERROR":
return { ...state, loading: false, error: action.error };
}
}
const [state, dispatch] = useReducer(reducer, {
items: [],
loading: false,
error: null,
});
// Dispatch actions
dispatch({ type: "FETCH_START" });
```
### 2. Custom Hooks
#### Extraction pattern
Extract a custom hook when:
- Two or more components share the same stateful logic
- A component's hook logic is complex enough to deserve its own name and tests
- You want to abstract away an external API (localStorage, WebSocket, etc.)
**Rules:**
- Name must start with `use`
- Can call other hooks (unlike regular functions)
- Each call gets its own independent state
#### Practical examples
```tsx
// useLocalStorage — persist state to localStorage
function useLocalStorage<T>(key: string, initialValue: T) {
const [stored, setStored] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback(
(value: T | ((prev: T) => T)) => {
setStored((prev) => {
const next = value instanceof Function ? value(prev) : value;
window.localStorage.setItem(key, JSON.stringify(next));
return next;
});
},
[key],
);
return [stored, setValue] as const;
}
// Usage
const [theme, setTheme] = useLocalStorage("theme", "light");
```
```tsx
// useDebounce — debounce a rapidly changing value
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]);
```
```tsx
// useFetch — generic data fetching hook
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
fetch(url, { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then((json) => setData(json as T))
.catch((err) => {
if (err.name !== "AbortError") setError(err);
})
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, error, loading };
}
// Usage
const { data: users, loading, error } = useFetch<User[]>("/api/users");
```
### 3. Component Patterns
#### Compound components
```tsx
// Components that work together, sharing implicit state
interface TabsContextType {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextType | null>(null);
function Tabs({ defaultTab, children }: { defaultTab: string; children: ReactNode }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<div className="card" onClick={() => onSelect?.(user)}>
<h3>{user.name}</h3>
<p>{user.email}</p>
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div role="tablist">{children}</div>
</TabsContext.Provider>
);
}
function TabTrigger({ value, children }: { value: string; children: ReactNode }) {
const ctx = useContext(TabsContext)!;
return (
<button
role="tab"
aria-selected={ctx.activeTab === value}
onClick={() => ctx.setActiveTab(value)}
>
{children}
</button>
);
}
function TabContent({ value, children }: { value: string; children: ReactNode }) {
const ctx = useContext(TabsContext)!;
if (ctx.activeTab !== value) return null;
return <div role="tabpanel">{children}</div>;
}
// Attach sub-components
Tabs.Trigger = TabTrigger;
Tabs.Content = TabContent;
// Usage
<Tabs defaultTab="settings">
<Tabs.Trigger value="profile">Profile</Tabs.Trigger>
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
<Tabs.Content value="profile"><ProfileForm /></Tabs.Content>
<Tabs.Content value="settings"><SettingsForm /></Tabs.Content>
</Tabs>
```
#### Render props
```tsx
interface MousePosition {
x: number;
y: number;
}
function MouseTracker({ render }: { render: (pos: MousePosition) => ReactNode }) {
const [pos, setPos] = useState<MousePosition>({ x: 0, y: 0 });
useEffect(() => {
const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener("mousemove", handler);
return () => window.removeEventListener("mousemove", handler);
}, []);
return <>{render(pos)}</>;
}
// Usage
<MouseTracker render={({ x, y }) => <span>Mouse: {x}, {y}</span>} />
```
#### Controlled vs uncontrolled
```tsx
// Controlled — parent owns the state
interface ControlledInputProps {
value: string;
onChange: (value: string) => void;
}
function ControlledInput({ value, onChange }: ControlledInputProps) {
return <input value={value} onChange={(e) => onChange(e.target.value)} />;
}
// Uncontrolled — component owns the state, parent reads via ref or callback
function UncontrolledInput({ defaultValue }: { defaultValue?: string }) {
const ref = useRef<HTMLInputElement>(null);
return <input ref={ref} defaultValue={defaultValue} />;
}
// Flexible pattern — supports both controlled and uncontrolled
function FlexibleInput({
value: controlledValue,
defaultValue = "",
onChange,
}: {
value?: string;
defaultValue?: string;
onChange?: (value: string) => void;
}) {
const [internalValue, setInternalValue] = useState(defaultValue);
const isControlled = controlledValue !== undefined;
const value = isControlled ? controlledValue : internalValue;
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
if (!isControlled) setInternalValue(e.target.value);
onChange?.(e.target.value);
}
return <input value={value} onChange={handleChange} />;
}
```
### 4. Context
#### Provider pattern with separate state and dispatch
```tsx
// Split context to prevent unnecessary re-renders
interface AppState {
user: User | null;
theme: "light" | "dark";
}
type AppAction =
| { type: "SET_USER"; user: User | null }
| { type: "TOGGLE_THEME" };
const AppStateContext = createContext<AppState | null>(null);
const AppDispatchContext = createContext<React.Dispatch<AppAction> | null>(null);
function appReducer(state: AppState, action: AppAction): AppState {
switch (action.type) {
case "SET_USER":
return { ...state, user: action.user };
case "TOGGLE_THEME":
return { ...state, theme: state.theme === "light" ? "dark" : "light" };
}
}
function AppProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(appReducer, {
user: null,
theme: "light",
});
return (
<AppStateContext.Provider value={state}>
<AppDispatchContext.Provider value={dispatch}>
{children}
</AppDispatchContext.Provider>
</AppStateContext.Provider>
);
}
// Typed hooks for consumers
function useAppState() {
const ctx = useContext(AppStateContext);
if (!ctx) throw new Error("useAppState must be used within AppProvider");
return ctx;
}
function useAppDispatch() {
const ctx = useContext(AppDispatchContext);
if (!ctx) throw new Error("useAppDispatch must be used within AppProvider");
return ctx;
}
```
**Why split?** Components that only dispatch actions (buttons) do not re-render when state changes. Only components that read state re-render.
#### Context splitting for performance
```tsx
// Instead of one giant context with everything:
const UserContext = createContext<User | null>(null);
const ThemeContext = createContext<"light" | "dark">("light");
const NotificationContext = createContext<Notification[]>([]);
// Components subscribe only to the context they need
function Avatar() {
const user = useContext(UserContext); // Only re-renders when user changes
return <img src={user?.avatar} />;
}
```
### 5. Error Boundaries
#### Class-based error boundary
```tsx
class ErrorBoundary extends React.Component<
{ children: ReactNode; fallback: ReactNode },
{ hasError: boolean; error: Error | null }
> {
constructor(props: { children: ReactNode; fallback: ReactNode }) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error("Error boundary caught:", error, info.componentStack);
// Send to error tracking service
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
```
#### react-error-boundary library (recommended)
```tsx
import { ErrorBoundary, useErrorBoundary } from "react-error-boundary";
function ErrorFallback({
error,
resetErrorBoundary,
}: {
error: Error;
resetErrorBoundary: () => void;
}) {
return (
<div role="alert">
<h2>Something went wrong</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
// Usage
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// Reset app state if needed
}}
resetKeys={[userId]} // Auto-reset when these values change
>
<Dashboard />
</ErrorBoundary>
// Programmatic error throwing from child
function SaveButton() {
const { showBoundary } = useErrorBoundary();
async function handleSave() {
try {
await saveData();
} catch (error) {
showBoundary(error); // Propagate to nearest ErrorBoundary
}
}
return <button onClick={handleSave}>Save</button>;
}
```
### 6. Suspense
#### Suspense boundaries with lazy loading
```tsx
import { Suspense, lazy } from "react";
// Code-split heavy components
const HeavyChart = lazy(() => import("./heavy-chart"));
const AdminPanel = lazy(() => import("./admin-panel"));
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
<Suspense fallback={<div>Loading admin panel...</div>}>
<AdminPanel />
</Suspense>
</div>
);
}
```
### Hooks
#### Suspense with data fetching (React 19+ / framework integration)
```tsx
// useState
const [count, setCount] = useState(0);
// useEffect
useEffect(() => {
const subscription = subscribe();
return () => subscription.unsubscribe();
}, [dependency]);
// useMemo
const expensive = useMemo(() => compute(data), [data]);
// useCallback
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
```
### Custom Hooks
```tsx
function useUser(id: string) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetchUser(id)
.then(setUser)
.finally(() => setLoading(false));
}, [id]);
return { user, loading };
}
```
### Context Pattern
```tsx
const UserContext = createContext<User | null>(null);
export function UserProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
// With a Suspense-compatible data source (React Query, Next.js, Relay)
function ProjectList() {
return (
<UserContext.Provider value={user}>
{children}
</UserContext.Provider>
<Suspense fallback={<ProjectListSkeleton />}>
<ProjectListContent />
</Suspense>
);
}
export function useUser() {
const context = useContext(UserContext);
if (!context) throw new Error('useUser must be within UserProvider');
return context;
// The data-fetching component suspends while loading
function ProjectListContent() {
const { data } = useSuspenseQuery({
queryKey: ["projects"],
queryFn: fetchProjects,
});
return (
<ul>
{data.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
```
#### Named exports with lazy
```tsx
// For named exports, wrap in a default export adapter
const UserSettings = lazy(() =>
import("./user-settings").then((mod) => ({ default: mod.UserSettings })),
);
```
### 7. Performance
#### React.memo
```tsx
// Only re-renders when props change (shallow comparison)
const ExpensiveList = React.memo(function ExpensiveList({
items,
onSelect,
}: {
items: Item[];
onSelect: (item: Item) => void;
}) {
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onSelect(item)}>
{item.name}
</li>
))}
</ul>
);
});
// Custom comparison
const Chart = React.memo(ChartComponent, (prev, next) => {
return prev.data.length === next.data.length && prev.title === next.title;
});
```
#### useMemo and useCallback together
```tsx
function ParentComponent({ items }: { items: Item[] }) {
// Memoize expensive derived data
const sortedItems = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items],
);
// Stable function reference for memoized child
const handleSelect = useCallback((item: Item) => {
console.log("Selected:", item.id);
}, []);
// ExpensiveList only re-renders when sortedItems or handleSelect change
return <ExpensiveList items={sortedItems} onSelect={handleSelect} />;
}
```
#### Key prop optimization
```tsx
// BAD: using index as key — breaks state when list order changes
{items.map((item, index) => <Item key={index} data={item} />)}
// GOOD: stable unique key
{items.map((item) => <Item key={item.id} data={item} />)}
// Force remount: change key to reset component state
<ProfileForm key={userId} userId={userId} />
// When userId changes, the form unmounts and remounts with fresh state
```
#### Virtualization for large lists
```tsx
import { useVirtualizer } from "@tanstack/react-virtual";
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // Estimated row height in px
overscan: 5, // Extra rows rendered above/below viewport
});
return (
<div ref={parentRef} style={{ height: "400px", overflow: "auto" }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative" }}>
{virtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
style={{
position: "absolute",
top: 0,
transform: `translateY(${virtualRow.start}px)`,
height: `${virtualRow.size}px`,
width: "100%",
}}
>
{items[virtualRow.index].name}
</div>
))}
</div>
</div>
);
}
```
---
## Best Practices
1. Keep components small and focused
2. Use TypeScript for props
3. Memoize expensive computations
4. Clean up effects properly
5. Lift state up when needed
1. **Keep components small and single-purpose** — if a component exceeds ~100 lines or handles multiple concerns, extract sub-components or custom hooks. Name components after what they render, not what they do.
2. **Use TypeScript interfaces for all props** — define explicit prop types. Avoid `any`. Use discriminated unions for props that change based on a variant. Export prop types for reuse.
3. **Clean up all effects** — return a cleanup function from every `useEffect` that subscribes to events, starts timers, or creates abort controllers. Missing cleanups cause memory leaks and stale state bugs.
4. **Derive state instead of syncing it** — if a value can be computed from props or other state, compute it during render (or with `useMemo`). Never `useEffect` to sync derived state — it causes an extra render.
5. **Lift state to the lowest common ancestor** — not higher. State should live in the closest parent that needs it. If siblings need shared state, lift to their parent. If distant components need it, use context.
6. **Use `useCallback` and `React.memo` together, not alone**`useCallback` only helps when the function is passed to a memoized child. `React.memo` only helps when the parent actually passes stable props. Using one without the other is wasted effort.
7. **Prefer composition over prop drilling** — instead of passing props through 5 levels, restructure so the parent renders the child directly (component composition) or use context for truly global state.
8. **Handle all async states** — every data-fetching component should handle loading, error, and empty states. Use Suspense and Error Boundaries for declarative handling. Never leave a component that shows nothing while loading.
---
## Common Pitfalls
- **Missing dependencies in hooks**: Include all dependencies
- **State updates on unmounted**: Use cleanup functions
- **Prop drilling**: Use context or composition
1. **Missing or wrong dependency arrays** — forgetting to add a dependency to `useEffect`/`useMemo`/`useCallback` causes stale closures. Adding too many causes unnecessary re-runs. Use the `react-hooks/exhaustive-deps` ESLint rule.
2. **Setting state during render** — calling `setState` unconditionally in the render body causes infinite re-render loops. State updates should be in event handlers, effects, or callbacks — never at the top level of the component function.
3. **Prop drilling through many layers** — passing a prop through 4+ intermediate components that do not use it. Fix with composition (restructuring the component tree), context (for shared state), or a state management library.
4. **Creating objects/arrays in JSX props**`<Child style={{ color: "red" }} />` creates a new object every render, defeating `React.memo`. Hoist constants outside the component or use `useMemo`.
5. **Using `useEffect` for derived state** — syncing state with `useEffect(() => setFullName(first + last), [first, last])` causes double renders. Just compute it: `const fullName = first + last`. Use `useMemo` if the computation is expensive.
6. **Not handling race conditions in effects** — when a component fetches data based on a prop, fast prop changes can cause older responses to arrive after newer ones, displaying stale data. Use `AbortController` or a boolean flag to ignore stale responses.
---
## 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
@@ -0,0 +1,243 @@
# 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
+905 -54
View File
@@ -1,87 +1,938 @@
---
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.
---
# shadcn/ui
## Description
shadcn/ui component library patterns with Radix primitives and Tailwind styling.
## When to Use
- Building React component libraries
- Accessible UI components
- Customizable design systems
## When NOT to Use
- Non-React projects using Vue, Svelte, Angular, or other frameworks
- Projects already using a different component library such as MUI, Chakra UI, or Ant Design
- Vanilla HTML/CSS projects without a React build pipeline
---
## Core Patterns
### Button Component
### 1. Installation & Setup
```tsx
import { Button } from "@/components/ui/button"
**Initialize shadcn/ui in a Next.js or Vite project:**
<Button variant="default">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
```bash
# Initialize -- creates components.json and sets up paths
npx shadcn@latest init
# You will be prompted for:
# - Style (default or new-york)
# - Base color
# - CSS variables for colors (yes recommended)
# - Tailwind config path
# - Components alias path (@/components)
# - Utils alias path (@/lib/utils)
```
### Form with Validation
**Install individual components as needed:**
```bash
# Install specific components
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add dialog
npx shadcn@latest add form
npx shadcn@latest add input
npx shadcn@latest add table
npx shadcn@latest add toast
# Install multiple at once
npx shadcn@latest add button card input label textarea select
# List available components
npx shadcn@latest add
```
**Project structure after setup:**
```
src/
├── components/
│ └── ui/ # shadcn/ui components live here
│ ├── button.tsx
│ ├── card.tsx
│ ├── dialog.tsx
│ └── ...
├── lib/
│ └── utils.ts # cn() utility
└── app/
└── globals.css # CSS variables for theming
```
**The `cn()` utility -- the foundation of class merging:**
```ts
// lib/utils.ts (auto-generated)
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
```
### 2. Component Customization
**Extending an existing component with new variants:**
```tsx
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { Form, FormField, FormItem, FormLabel, FormControl } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
// components/ui/button.tsx -- add a "brand" variant
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const form = useForm({
resolver: zodResolver(schema),
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
// Custom variant added
brand: "bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
```
**Wrapping a shadcn component with project-specific defaults:**
```tsx
// components/app/submit-button.tsx
import { Button, type ButtonProps } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
interface SubmitButtonProps extends ButtonProps {
loading?: boolean;
}
export function SubmitButton({
children,
loading,
disabled,
className,
...props
}: SubmitButtonProps) {
return (
<Button
type="submit"
disabled={disabled || loading}
className={cn("min-w-[120px]", className)}
{...props}
>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{children}
</Button>
);
}
```
**Using `asChild` for composition:**
```tsx
import { Button } from "@/components/ui/button";
import Link from "next/link";
// Render as a Next.js Link instead of a <button>
<Button asChild>
<Link href="/dashboard">Go to Dashboard</Link>
</Button>
// Render as an anchor tag
<Button asChild variant="link">
<a href="https://example.com" target="_blank" rel="noopener noreferrer">
External Link
</a>
</Button>
```
### 3. Form Patterns
**Complete form with react-hook-form + zod validation:**
```tsx
"use client";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const contactSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Please enter a valid email"),
category: z.enum(["general", "support", "billing"], {
required_error: "Please select a category",
}),
message: z.string().min(10, "Message must be at least 10 characters"),
});
<Form {...form}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
</FormItem>
)}
/>
</Form>
type ContactFormValues = z.infer<typeof contactSchema>;
export function ContactForm() {
const form = useForm<ContactFormValues>({
resolver: zodResolver(contactSchema),
defaultValues: {
name: "",
email: "",
message: "",
},
});
async function onSubmit(values: ContactFormValues) {
// Handle form submission
console.log(values);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Your name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="you@example.com" {...field} />
</FormControl>
<FormDescription>We will never share your email.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="category"
render={({ field }) => (
<FormItem>
<FormLabel>Category</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="general">General Inquiry</SelectItem>
<SelectItem value="support">Technical Support</SelectItem>
<SelectItem value="billing">Billing</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel>Message</FormLabel>
<FormControl>
<Textarea
placeholder="How can we help?"
className="min-h-[120px] resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? "Sending..." : "Send Message"}
</Button>
</form>
</Form>
);
}
```
### Dialog
### 4. Data Table
**Column definitions with sorting and formatting:**
```tsx
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
"use client";
<Dialog>
<DialogTrigger asChild>
<Button>Open</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Title</DialogTitle>
</DialogHeader>
Content
</DialogContent>
</Dialog>
import { ColumnDef } from "@tanstack/react-table";
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface Payment {
id: string;
amount: number;
status: "pending" | "processing" | "success" | "failed";
email: string;
createdAt: Date;
}
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: "email",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Email
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const status = row.getValue("status") as string;
const variant = {
pending: "secondary",
processing: "outline",
success: "default",
failed: "destructive",
}[status] as "secondary" | "outline" | "default" | "destructive";
return <Badge variant={variant}>{status}</Badge>;
},
filterFn: (row, id, value) => value.includes(row.getValue(id)),
},
{
accessorKey: "amount",
header: () => <div className="text-right">Amount</div>,
cell: ({ row }) => {
const amount = parseFloat(row.getValue("amount"));
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
return <div className="text-right font-medium">{formatted}</div>;
},
},
{
id: "actions",
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(row.original.id)}>
Copy ID
</DropdownMenuItem>
<DropdownMenuItem>View details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
];
```
**DataTable component with filtering and pagination:**
```tsx
"use client";
import { useState } from "react";
import {
ColumnDef,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
getFilteredRowModel,
SortingState,
ColumnFiltersState,
useReactTable,
} from "@tanstack/react-table";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
searchKey?: string;
}
export function DataTable<TData, TValue>({
columns,
data,
searchKey,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
state: { sorting, columnFilters },
});
return (
<div className="space-y-4">
{searchKey && (
<Input
placeholder={`Filter by ${searchKey}...`}
value={(table.getColumn(searchKey)?.getFilterValue() as string) ?? ""}
onChange={(e) => table.getColumn(searchKey)?.setFilterValue(e.target.value)}
className="max-w-sm"
/>
)}
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
{table.getFilteredRowModel().rows.length} row(s) total
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
);
}
```
### 5. Dialog / Sheet / Drawer
**Controlled dialog with form:**
```tsx
"use client";
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function EditProfileDialog() {
const [open, setOpen] = useState(false);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
// Save profile...
setOpen(false); // Close after success
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline">Edit Profile</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>
Make changes to your profile. Click save when you are done.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">Name</Label>
<Input id="name" defaultValue="John Doe" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="username" className="text-right">Username</Label>
<Input id="username" defaultValue="@johndoe" className="col-span-3" />
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button type="submit">Save changes</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
```
**Sheet for side panels (mobile nav, filters):**
```tsx
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
import { Menu } from "lucide-react";
export function MobileNav() {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="md:hidden">
<Menu className="h-5 w-5" />
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-[280px]">
<SheetHeader>
<SheetTitle>Navigation</SheetTitle>
<SheetDescription>Browse the application.</SheetDescription>
</SheetHeader>
<nav className="mt-6 flex flex-col gap-2">
<a href="/" className="rounded-md px-3 py-2 text-sm hover:bg-accent">Home</a>
<a href="/about" className="rounded-md px-3 py-2 text-sm hover:bg-accent">About</a>
<a href="/settings" className="rounded-md px-3 py-2 text-sm hover:bg-accent">Settings</a>
</nav>
</SheetContent>
</Sheet>
);
}
```
**Confirmation dialog pattern (reusable):**
```tsx
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
interface ConfirmDialogProps {
trigger: React.ReactNode;
title: string;
description: string;
onConfirm: () => void;
destructive?: boolean;
}
export function ConfirmDialog({
trigger,
title,
description,
onConfirm,
destructive = false,
}: ConfirmDialogProps) {
return (
<AlertDialog>
<AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={onConfirm}
className={destructive ? "bg-destructive text-destructive-foreground hover:bg-destructive/90" : ""}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
```
### 6. Toast / Notifications
**Setup with Sonner (recommended approach):**
```bash
npx shadcn@latest add sonner
```
```tsx
// app/layout.tsx -- add the Toaster provider
import { Toaster } from "@/components/ui/sonner";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Toaster richColors position="bottom-right" />
</body>
</html>
);
}
```
```tsx
// Using toast anywhere in your app
import { toast } from "sonner";
function SaveButton() {
async function handleSave() {
try {
await saveData();
toast.success("Changes saved", {
description: "Your profile has been updated.",
});
} catch (error) {
toast.error("Failed to save", {
description: "Please try again later.",
});
}
}
return <Button onClick={handleSave}>Save</Button>;
}
// Toast variants
toast("Default notification");
toast.success("Operation completed");
toast.error("Something went wrong");
toast.warning("Please review your input");
toast.info("New version available");
// Toast with action
toast("File deleted", {
action: {
label: "Undo",
onClick: () => restoreFile(),
},
});
// Promise toast -- shows loading, success, and error states
toast.promise(fetchData(), {
loading: "Loading data...",
success: "Data loaded successfully",
error: "Failed to load data",
});
```
### 7. Theme System
**CSS variables in globals.css:**
```css
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
}
}
```
**Creating a custom color theme:**
```css
/* Add a custom "ocean" theme alongside light and dark */
.theme-ocean {
--background: 210 50% 10%;
--foreground: 195 80% 90%;
--primary: 195 90% 50%;
--primary-foreground: 210 50% 10%;
--secondary: 200 40% 20%;
--secondary-foreground: 195 80% 90%;
--muted: 200 30% 18%;
--muted-foreground: 195 30% 60%;
--accent: 180 60% 40%;
--accent-foreground: 195 80% 90%;
--border: 200 30% 25%;
--input: 200 30% 25%;
--ring: 195 90% 50%;
}
```
**Theme provider for Next.js:**
```tsx
// components/theme-provider.tsx
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
```
```tsx
// app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
);
}
```
```tsx
// components/theme-toggle.tsx
"use client";
import { useTheme } from "next-themes";
import { Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function ThemeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
```
---
## Best Practices
1. Use cn() for conditional classes
2. Compose primitives for custom components
3. Follow accessibility patterns
4. Customize via CSS variables
5. Use asChild for composition
1. **Install components individually** -- only add what you need. Each component is copied into your codebase, so unused components add dead code.
2. **Customize at the source** -- since components live in your `components/ui/` directory, modify them directly rather than wrapping with overrides. This is the intended workflow.
3. **Use `cn()` for all conditional styling** -- it merges Tailwind classes correctly, avoiding conflicts. Never concatenate class strings manually.
4. **Keep forms type-safe end to end** -- define a zod schema, infer the TypeScript type from it, and pass it to `useForm<T>`. This gives you validation and type safety in one place.
5. **Use `asChild` for semantic HTML** -- when a Button should be a link, or a DialogTrigger should be a custom component, use `asChild` to avoid nested interactive elements.
6. **Follow the CSS variable naming convention** -- shadcn/ui expects HSL values without the `hsl()` wrapper (e.g., `220 14% 96%`). The `hsl()` is applied in Tailwind config.
7. **Wrap layout-level providers once** -- place `ThemeProvider`, `Toaster`, and other providers in the root layout. Do not nest them in individual pages.
8. **Prefer Sonner over the legacy toast** -- the Sonner integration is simpler, supports rich colors, promise toasts, and requires less boilerplate than the older toast component.
## Common Pitfalls
- **Missing cn import**: Import from lib/utils
- **Wrong composition**: Use asChild properly
- **Hardcoded colors**: Use CSS variables
1. **Missing `cn` import** -- every component uses `cn()` from `@/lib/utils`. If you see class merging issues, verify this import exists and uses both `clsx` and `tailwind-merge`.
2. **Incorrect `asChild` usage** -- `asChild` merges props onto the immediate child. If you wrap the child in a fragment or extra div, the props will not pass through correctly.
3. **Hardcoded colors instead of CSS variables** -- using `bg-blue-500` instead of `bg-primary` bypasses the theme system. Always use semantic token names so dark mode and custom themes work.
4. **Forgetting `"use client"` directive** -- shadcn/ui components using hooks (Dialog, Form, Sheet, etc.) require the `"use client"` directive in Next.js App Router. The UI primitives themselves include it, but your page-level components that use them may also need it.
5. **Not handling controlled state in dialogs** -- for dialogs that contain forms, use the controlled `open` / `onOpenChange` pattern so you can close the dialog programmatically after submission.
6. **Stale component versions** -- since components are copied into your project, they do not auto-update. Periodically check the shadcn/ui docs for fixes and re-run `npx shadcn@latest add <component>` to pull updates (review the diff before accepting).
## 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
@@ -0,0 +1,242 @@
# 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
+558 -52
View File
@@ -1,82 +1,588 @@
---
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.
---
# Tailwind CSS
## Description
Tailwind CSS utility-first styling with responsive design and component patterns.
## When to Use
- Styling React/Next.js components
- Responsive design
- Rapid UI development
## When NOT to Use
- Backend-only projects with no frontend or UI layer
- Projects using CSS-in-JS solutions like styled-components or Emotion
- Non-web applications such as CLI tools, mobile native apps, or desktop utilities
---
## Core Patterns
### Layout
### 1. Responsive Design
```html
<!-- Flexbox -->
<div class="flex items-center justify-between gap-4">
<div>Left</div>
<div>Right</div>
</div>
Tailwind uses a mobile-first breakpoint system. Styles without a prefix apply to all screen sizes; prefixed styles apply at that breakpoint and above.
<!-- Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div>Item</div>
</div>
| Breakpoint | Min Width | Typical Target |
|------------|-----------|----------------|
| `sm` | 640px | Large phones |
| `md` | 768px | Tablets |
| `lg` | 1024px | Laptops |
| `xl` | 1280px | Desktops |
| `2xl` | 1536px | Large screens |
<!-- Container -->
<div class="container mx-auto px-4">
Content
</div>
```tsx
// Mobile-first responsive text and spacing
function HeroSection() {
return (
<section className="px-4 py-8 sm:px-6 sm:py-12 md:px-8 md:py-16 lg:py-24">
<h1 className="text-2xl font-bold sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl">
Build faster with Tailwind
</h1>
<p className="mt-4 text-sm text-gray-600 sm:text-base md:text-lg lg:max-w-2xl">
A utility-first CSS framework for rapid UI development.
</p>
</section>
);
}
```
### Responsive
```html
<!-- Mobile-first breakpoints -->
<div class="w-full md:w-1/2 lg:w-1/3">
<!-- sm: 640px, md: 768px, lg: 1024px, xl: 1280px -->
</div>
<h1 class="text-xl md:text-2xl lg:text-4xl">
Responsive text
</h1>
```tsx
// Responsive container with constrained width
function PageContainer({ children }: { children: React.ReactNode }) {
return (
<div className="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8">
{children}
</div>
);
}
```
### States
```tsx
// Responsive visibility - show/hide elements at breakpoints
function ResponsiveNav() {
return (
<nav>
{/* Mobile hamburger - hidden on desktop */}
<button className="block md:hidden">
<MenuIcon />
</button>
{/* Desktop nav links - hidden on mobile */}
<div className="hidden md:flex md:items-center md:gap-6">
<a href="/about">About</a>
<a href="/pricing">Pricing</a>
<a href="/docs">Docs</a>
</div>
</nav>
);
}
```
```html
<button class="
bg-blue-500 hover:bg-blue-600
focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
disabled:opacity-50 disabled:cursor-not-allowed
">
Button
### 2. Dark Mode
**Class strategy** (recommended) -- toggle via a `dark` class on the `<html>` element:
```js
// tailwind.config.js
module.exports = {
darkMode: "class",
};
```
```tsx
// Theme toggle component
function ThemeToggle() {
const [dark, setDark] = useState(false);
function toggle() {
setDark(!dark);
document.documentElement.classList.toggle("dark");
}
return (
<button
onClick={toggle}
className="rounded-full p-2 text-gray-600 hover:bg-gray-100
dark:text-gray-300 dark:hover:bg-gray-800"
>
{dark ? <SunIcon /> : <MoonIcon />}
</button>
);
}
```
```tsx
// Dark mode with CSS variables for flexible theming
function Card({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm
dark:border-gray-700 dark:bg-gray-900 dark:shadow-gray-900/20">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{title}
</h3>
<div className="mt-2 text-gray-600 dark:text-gray-400">
{children}
</div>
</div>
);
}
```
**Media strategy** -- follows the OS preference automatically:
```js
// tailwind.config.js
module.exports = {
darkMode: "media",
};
```
**CSS variables approach** for more granular theme control:
```css
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222 47% 11%;
--primary: 221 83% 53%;
--muted: 210 40% 96%;
}
.dark {
--background: 222 47% 11%;
--foreground: 210 40% 98%;
--primary: 217 91% 60%;
--muted: 217 33% 17%;
}
}
```
```js
// tailwind.config.js -- reference the CSS variables
module.exports = {
theme: {
extend: {
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: "hsl(var(--primary))",
muted: "hsl(var(--muted))",
},
},
},
};
```
### 3. Layout Patterns
**Sidebar layout:**
```tsx
function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen">
{/* Sidebar - fixed width, scrollable */}
<aside className="hidden w-64 flex-shrink-0 overflow-y-auto border-r
border-gray-200 bg-gray-50 p-4 dark:border-gray-700
dark:bg-gray-900 lg:block">
<nav className="flex flex-col gap-1">
<SidebarLink href="/dashboard" icon={<HomeIcon />} label="Home" />
<SidebarLink href="/settings" icon={<GearIcon />} label="Settings" />
</nav>
</aside>
{/* Main content - fills remaining space */}
<main className="flex-1 overflow-y-auto p-6">{children}</main>
</div>
);
}
```
**Card grid:**
```tsx
function CardGrid({ items }: { items: CardItem[] }) {
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{items.map((item) => (
<div
key={item.id}
className="rounded-lg border border-gray-200 bg-white p-5 shadow-sm
transition-shadow hover:shadow-md dark:border-gray-700
dark:bg-gray-800"
>
<h3 className="font-medium text-gray-900 dark:text-gray-100">
{item.title}
</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
{item.description}
</p>
</div>
))}
</div>
);
}
```
**Centered content with max-width:**
```tsx
function ArticleLayout({ children }: { children: React.ReactNode }) {
return (
<article className="mx-auto max-w-prose px-4 py-8">
<div className="prose prose-gray dark:prose-invert lg:prose-lg">
{children}
</div>
</article>
);
}
```
**Sticky header with content scroll:**
```tsx
function AppShell({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen flex-col">
<header className="sticky top-0 z-40 flex h-16 items-center border-b
border-gray-200 bg-white/80 px-6 backdrop-blur-sm
dark:border-gray-800 dark:bg-gray-950/80">
<Logo />
<nav className="ml-auto flex items-center gap-4">
<NavLinks />
</nav>
</header>
<main className="flex-1 overflow-y-auto">{children}</main>
</div>
);
}
```
### 4. Component Styling
**Button variants using a helper:**
```tsx
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-blue-600 text-white hover:bg-blue-700",
secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-100",
outline: "border border-gray-300 bg-transparent hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-800",
ghost: "hover:bg-gray-100 dark:hover:bg-gray-800",
destructive: "bg-red-600 text-white hover:bg-red-700",
},
size: {
sm: "h-8 px-3 text-xs",
md: "h-10 px-4",
lg: "h-12 px-6 text-base",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
}
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button className={cn(buttonVariants({ variant, size }), className)} {...props} />
);
}
```
**Form input:**
```tsx
function FormInput({ label, error, ...props }: InputProps) {
return (
<div className="space-y-1.5">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
{label}
</label>
<input
className={cn(
"block w-full rounded-md border px-3 py-2 text-sm shadow-sm",
"placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500",
"dark:bg-gray-900 dark:text-gray-100 dark:placeholder:text-gray-500",
error
? "border-red-500 focus:ring-red-500"
: "border-gray-300 dark:border-gray-600"
)}
{...props}
/>
{error && <p className="text-xs text-red-600">{error}</p>}
</div>
);
}
```
**Navigation with active state:**
```tsx
function NavLink({ href, active, children }: NavLinkProps) {
return (
<a
href={href}
className={cn(
"rounded-md px-3 py-2 text-sm font-medium transition-colors",
active
? "bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-100"
)}
>
{children}
</a>
);
}
```
### 5. Animations & Transitions
**Built-in animations:**
```tsx
// Spin for loading indicators
<svg className="h-5 w-5 animate-spin text-white" viewBox="0 0 24 24">...</svg>
// Pulse for skeleton loaders
function Skeleton() {
return <div className="h-4 w-full animate-pulse rounded bg-gray-200 dark:bg-gray-700" />;
}
// Bounce for attention
<div className="animate-bounce">
<ArrowDownIcon />
</div>
// Ping for notification badges
<span className="relative flex h-3 w-3">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75" />
<span className="relative inline-flex h-3 w-3 rounded-full bg-red-500" />
</span>
```
**Transitions for interactive elements:**
```tsx
// Smooth hover transitions
function HoverCard({ children }: { children: React.ReactNode }) {
return (
<div className="rounded-lg border bg-white p-4 shadow-sm transition-all
duration-200 ease-in-out hover:-translate-y-1 hover:shadow-lg
dark:border-gray-700 dark:bg-gray-800">
{children}
</div>
);
}
// Color and opacity transitions
<button className="bg-blue-500 text-white transition-colors duration-150
hover:bg-blue-600 active:bg-blue-700">
Submit
</button>
```
### Dark Mode
```html
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
Content
// Scale on hover
<div className="transform transition-transform duration-200 hover:scale-105">
<img src={src} alt={alt} className="rounded-lg" />
</div>
```
**Custom keyframes in config:**
```js
// tailwind.config.js
module.exports = {
theme: {
extend: {
keyframes: {
"fade-in": {
"0%": { opacity: "0", transform: "translateY(8px)" },
"100%": { opacity: "1", transform: "translateY(0)" },
},
"slide-in-right": {
"0%": { transform: "translateX(100%)" },
"100%": { transform: "translateX(0)" },
},
"scale-in": {
"0%": { opacity: "0", transform: "scale(0.95)" },
"100%": { opacity: "1", transform: "scale(1)" },
},
},
animation: {
"fade-in": "fade-in 0.3s ease-out",
"slide-in-right": "slide-in-right 0.3s ease-out",
"scale-in": "scale-in 0.2s ease-out",
},
},
},
};
```
```tsx
// Using custom animations
<div className="animate-fade-in">Content that fades in</div>
<aside className="animate-slide-in-right">Sidebar panel</aside>
```
### 6. Custom Theme
**Extending tailwind.config.js:**
```js
// tailwind.config.js
const { fontFamily } = require("tailwindcss/defaultTheme");
module.exports = {
content: ["./src/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"],
darkMode: "class",
theme: {
extend: {
colors: {
brand: {
50: "#eff6ff",
100: "#dbeafe",
200: "#bfdbfe",
300: "#93c5fd",
400: "#60a5fa",
500: "#3b82f6",
600: "#2563eb",
700: "#1d4ed8",
800: "#1e40af",
900: "#1e3a8a",
950: "#172554",
},
},
fontFamily: {
sans: ["Inter", ...fontFamily.sans],
mono: ["JetBrains Mono", ...fontFamily.mono],
},
spacing: {
18: "4.5rem",
88: "22rem",
128: "32rem",
},
borderRadius: {
"4xl": "2rem",
},
fontSize: {
"2xs": ["0.625rem", { lineHeight: "0.75rem" }],
},
},
},
plugins: [
require("@tailwindcss/typography"),
require("@tailwindcss/forms"),
require("@tailwindcss/container-queries"),
],
};
```
### 7. Performance
**Content configuration** -- ensure only used classes ship to production:
```js
// tailwind.config.js
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
],
};
```
**Avoid dynamic class construction** -- Tailwind cannot detect dynamically built class names:
```tsx
// BAD -- Tailwind will NOT include these classes
const color = "red";
<div className={`bg-${color}-500`}>...</div>
// GOOD -- use complete class names so Tailwind can detect them
const bgColor = isError ? "bg-red-500" : "bg-green-500";
<div className={bgColor}>...</div>
```
**Safelist for truly dynamic values:**
```js
// tailwind.config.js
module.exports = {
safelist: [
"bg-red-500",
"bg-green-500",
"bg-blue-500",
{ pattern: /^text-(red|green|blue)-(400|500|600)$/ },
],
};
```
**Keep class strings readable with cn():**
```ts
// lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
```
```tsx
// Composing classes cleanly
<div className={cn(
"rounded-lg border p-4",
isActive && "border-blue-500 bg-blue-50",
isDisabled && "pointer-events-none opacity-50",
className
)}>
```
---
## Best Practices
1. Use consistent spacing scale
2. Mobile-first design
3. Extract repeated patterns to components
4. Use @apply sparingly
5. Leverage dark mode utilities
1. **Mobile-first always** -- write base styles for mobile, then layer breakpoint prefixes for larger screens. Never design desktop-down.
2. **Use the spacing scale consistently** -- stick to Tailwind's default scale (4, 8, 12, 16...) rather than arbitrary values. Use `space-y-*` and `gap-*` instead of individual margins.
3. **Extract repeated patterns to components** -- when the same set of classes appears three or more times, create a React component rather than duplicating the class string.
4. **Use `@apply` sparingly** -- only for styles that cannot live in a component, such as global prose styles or third-party element overrides. Overusing `@apply` defeats the utility-first approach.
5. **Prefer `cn()` / `twMerge` for conditional classes** -- avoids class conflicts and keeps logic readable compared to string template concatenation.
6. **Use CSS variables for theme tokens** -- allows runtime theme switching and integrates well with dark mode, while keeping Tailwind as the styling layer.
7. **Group related utilities logically** -- order classes as: layout, sizing, spacing, typography, colors, borders, effects, transitions. Consistent ordering improves readability.
8. **Enable the typography plugin for prose content** -- `@tailwindcss/typography` provides sensible defaults for rendered markdown or CMS content without manual styling.
## Common Pitfalls
- **Too many classes**: Extract to components
- **Inconsistent spacing**: Stick to scale
- **Missing responsive**: Test all breakpoints
1. **Dynamic class name construction** -- `bg-${color}-500` will not work because Tailwind scans source files statically. Always use complete, literal class names.
2. **Forgetting content paths** -- if a class is not being generated, check that `content` in `tailwind.config.js` includes all files where Tailwind classes are used, including component libraries.
3. **Class conflicts without twMerge** -- `className="p-4 p-6"` applies both; the result depends on CSS source order, not the order in the string. Use `twMerge` to resolve conflicts predictably.
4. **Overusing arbitrary values** -- `w-[347px]` bypasses the design system. If you find many arbitrary values, extend the theme instead.
5. **Not testing responsive breakpoints** -- always verify layouts at each breakpoint. Use browser dev tools' responsive mode or resize the viewport during development.
6. **Ignoring dark mode from the start** -- adding dark mode later requires touching every component. Apply `dark:` variants alongside initial styling to avoid large retrofits.
## 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
@@ -0,0 +1,231 @@
# Tailwind CSS UI Pattern Recipes
Copy-paste patterns for common UI components. All examples use Tailwind v3+ utility classes.
---
## Responsive Navbar
```html
<nav class="bg-white shadow">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 items-center justify-between">
<div class="flex items-center gap-8">
<a href="/" class="text-xl font-bold text-gray-900">Logo</a>
<div class="hidden md:flex md:gap-6">
<a href="#" class="text-sm font-medium text-gray-700 hover:text-gray-900">Dashboard</a>
<a href="#" class="text-sm font-medium text-gray-500 hover:text-gray-900">Projects</a>
<a href="#" class="text-sm font-medium text-gray-500 hover:text-gray-900">Settings</a>
</div>
</div>
<div class="flex items-center gap-4">
<button class="rounded-full bg-gray-100 p-2 text-gray-600 hover:bg-gray-200">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6 6 0 10-12 0v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0a3 3 0 11-6 0m6 0H9" />
</svg>
</button>
<img class="h-8 w-8 rounded-full" src="https://via.placeholder.com/32" alt="Avatar" />
</div>
<!-- Mobile menu button -->
<button class="md:hidden rounded p-2 text-gray-600 hover:bg-gray-100">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
</nav>
```
---
## Card Grid
```html
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<!-- Card -->
<div class="overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm
transition hover:shadow-md">
<img class="h-48 w-full object-cover" src="https://via.placeholder.com/400x200" alt="" />
<div class="p-5">
<h3 class="text-lg font-semibold text-gray-900">Card Title</h3>
<p class="mt-2 text-sm text-gray-600">
Brief description of the card content goes here.
</p>
<div class="mt-4 flex items-center justify-between">
<span class="inline-flex items-center rounded-full bg-blue-50 px-2.5 py-0.5
text-xs font-medium text-blue-700">Category</span>
<a href="#" class="text-sm font-medium text-blue-600 hover:text-blue-500">
View details &rarr;
</a>
</div>
</div>
</div>
<!-- Repeat cards... -->
</div>
```
---
## Hero Section
```html
<section class="bg-white">
<div class="mx-auto max-w-7xl px-4 py-24 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl text-center">
<span class="inline-block rounded-full bg-blue-50 px-3 py-1 text-sm
font-medium text-blue-700">New Release</span>
<h1 class="mt-4 text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
Build faster with modern tools
</h1>
<p class="mt-6 text-lg leading-8 text-gray-600">
A concise value proposition that explains what the product does
and why users should care.
</p>
<div class="mt-10 flex items-center justify-center gap-4">
<a href="#" class="rounded-lg bg-blue-600 px-6 py-3 text-sm font-semibold
text-white shadow-sm hover:bg-blue-500">Get started</a>
<a href="#" class="text-sm font-semibold text-gray-900 hover:text-gray-700">
Learn more &rarr;
</a>
</div>
</div>
</div>
</section>
```
---
## Form Layout
```html
<form class="mx-auto max-w-lg space-y-6">
<div>
<label for="name" class="block text-sm font-medium text-gray-700">Full Name</label>
<input type="text" id="name" name="name"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2
text-sm shadow-sm placeholder:text-gray-400
focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" />
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
<input type="email" id="email" name="email"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2
text-sm shadow-sm placeholder:text-gray-400
focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" />
<p class="mt-1 text-sm text-gray-500">We'll never share your email.</p>
</div>
<div>
<label for="message" class="block text-sm font-medium text-gray-700">Message</label>
<textarea id="message" name="message" rows="4"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2
text-sm shadow-sm placeholder:text-gray-400
focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"></textarea>
</div>
<div class="flex items-center justify-end gap-3">
<button type="button" class="rounded-md px-4 py-2 text-sm font-medium
text-gray-700 hover:bg-gray-50">Cancel</button>
<button type="submit" class="rounded-md bg-blue-600 px-4 py-2 text-sm
font-semibold text-white shadow-sm
hover:bg-blue-500">Submit</button>
</div>
</form>
```
---
## Modal Overlay
```html
<!-- Backdrop -->
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<!-- Modal -->
<div class="w-full max-w-md rounded-xl bg-white p-6 shadow-xl">
<div class="flex items-start justify-between">
<h2 class="text-lg font-semibold text-gray-900">Confirm Action</h2>
<button class="rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<p class="mt-3 text-sm text-gray-600">
Are you sure you want to proceed? This action cannot be undone.
</p>
<div class="mt-6 flex justify-end gap-3">
<button class="rounded-md px-4 py-2 text-sm font-medium text-gray-700
hover:bg-gray-50">Cancel</button>
<button class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold
text-white hover:bg-red-500">Delete</button>
</div>
</div>
</div>
```
---
## Sidebar Layout
```html
<div class="flex h-screen">
<!-- Sidebar -->
<aside class="hidden w-64 flex-shrink-0 border-r border-gray-200 bg-gray-50 lg:block">
<div class="flex h-16 items-center px-6">
<span class="text-lg font-bold text-gray-900">App Name</span>
</div>
<nav class="space-y-1 px-3 py-4">
<a href="#" class="flex items-center gap-3 rounded-md bg-blue-50 px-3 py-2
text-sm font-medium text-blue-700">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0h4" />
</svg>
Dashboard
</a>
<a href="#" class="flex items-center gap-3 rounded-md px-3 py-2 text-sm
font-medium text-gray-700 hover:bg-gray-100">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" />
</svg>
Users
</a>
<a href="#" class="flex items-center gap-3 rounded-md px-3 py-2 text-sm
font-medium text-gray-700 hover:bg-gray-100">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Settings
</a>
</nav>
</aside>
<!-- Main content -->
<main class="flex-1 overflow-y-auto bg-white">
<div class="px-6 py-8 lg:px-8">
<h1 class="text-2xl font-bold text-gray-900">Dashboard</h1>
<div class="mt-6">
<!-- Page content here -->
</div>
</div>
</main>
</div>
```
---
## Tips
- Use `transition` and `hover:` for interactive feedback
- Use `focus-visible:` instead of `focus:` for keyboard-only focus rings
- Use `dark:` variants when supporting dark mode
- Prefer `gap-*` over margin utilities for flex/grid spacing
- Use `max-w-7xl mx-auto px-4 sm:px-6 lg:px-8` as a standard container
+677 -54
View File
@@ -1,101 +1,724 @@
---
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.
---
# JavaScript
## Description
Modern JavaScript (ES6+) patterns and best practices for Node.js and browser environments.
## When to Use
- Working with JavaScript files (.js, .mjs)
- Browser scripting
- Node.js applications without TypeScript
## When NOT to Use
- TypeScript projects -- use the `languages/typescript` skill instead, which covers typed JavaScript patterns
- Python-only projects with no JavaScript components
---
## Core Patterns
### Modern Syntax
### 1. Modern Syntax
#### Destructuring (Nested, Defaults, Rest)
```javascript
// Destructuring
const { name, email } = user;
const [first, ...rest] = items;
// Object destructuring with defaults and rename
const { name, email, role = "user", address: { city } = {} } = user;
// Spread operator
const merged = { ...defaults, ...options };
const combined = [...array1, ...array2];
// Nested destructuring
const {
data: {
attributes: { title, body },
},
} = apiResponse;
// Template literals
const message = `Hello, ${name}!`;
// Array destructuring with rest
const [first, second, ...remaining] = items;
// Optional chaining and nullish coalescing
const city = user?.address?.city ?? 'Unknown';
// Swap variables
let a = 1, b = 2;
[a, b] = [b, a];
// Function parameter destructuring
function createUser({ name, email, role = "user" }) {
return { name, email, role, createdAt: new Date() };
}
```
### Async Patterns
#### Optional Chaining (?.)
```javascript
// Async/await
async function fetchData(url) {
const response = await fetch(url);
if (!response.ok) throw new Error('Fetch failed');
return response.json();
// Property access
const city = user?.address?.city;
// Method call
const uppercased = value?.toString?.();
// Array element
const firstItem = data?.items?.[0];
// Combine with nullish coalescing for defaults
const displayName = user?.profile?.displayName ?? user?.name ?? "Anonymous";
```
#### Nullish Coalescing (??)
```javascript
// Only falls through on null/undefined (not 0, "", false)
const port = config.port ?? 3000;
const name = input ?? "default";
// Contrast with || which falls through on all falsy values
const count = data.count ?? 0; // preserves 0
const count2 = data.count || 0; // replaces 0 with 0 (same here, but misleading)
const label = data.label ?? ""; // preserves ""
const label2 = data.label || "fallback"; // replaces "" with "fallback"
```
#### Logical Assignment (&&=, ||=, ??=)
```javascript
// ??= assigns only if null/undefined
user.name ??= "Anonymous";
// ||= assigns if falsy
config.retries ||= 3;
// &&= assigns only if truthy
user.session &&= refreshSession(user.session);
// Practical: initialize nested objects
const cache = {};
(cache.users ??= []).push(newUser);
```
---
### 2. Async Patterns
#### Promises
```javascript
function fetchJson(url) {
return fetch(url)
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
});
}
// Promise.all for parallel
const results = await Promise.all([
fetchData(url1),
fetchData(url2),
// Chaining
fetchJson("/api/user")
.then((user) => fetchJson(`/api/posts?userId=${user.id}`))
.then((posts) => console.log(posts))
.catch((error) => console.error("Failed:", error.message));
```
#### async/await
```javascript
async function loadUserDashboard(userId) {
const user = await fetchJson(`/api/users/${userId}`);
const posts = await fetchJson(`/api/users/${userId}/posts`);
return { user, posts };
}
```
#### Promise.all / allSettled / race / any
```javascript
// Promise.all -- fail fast on first rejection
const [users, posts, comments] = await Promise.all([
fetchJson("/api/users"),
fetchJson("/api/posts"),
fetchJson("/api/comments"),
]);
// Error handling
try {
const data = await fetchData(url);
} catch (error) {
console.error('Failed:', error.message);
// Promise.allSettled -- wait for all, get status of each
const results = await Promise.allSettled([
fetchJson("/api/fast"),
fetchJson("/api/slow"),
fetchJson("/api/flaky"),
]);
const successes = results
.filter((r) => r.status === "fulfilled")
.map((r) => r.value);
const failures = results
.filter((r) => r.status === "rejected")
.map((r) => r.reason);
// Promise.race -- first to settle wins
const result = await Promise.race([
fetchJson("/api/primary"),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), 5000)
),
]);
// Promise.any -- first to fulfill wins (ignores rejections)
const fastest = await Promise.any([
fetchJson("/api/mirror1"),
fetchJson("/api/mirror2"),
fetchJson("/api/mirror3"),
]);
```
#### AbortController
```javascript
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
return await response.json();
} finally {
clearTimeout(timeoutId);
}
}
// Cancellable request pattern
function createRequest(url) {
const controller = new AbortController();
return {
promise: fetch(url, { signal: controller.signal }),
cancel: () => controller.abort(),
};
}
```
### Array Methods
#### Async Iterators (for await...of)
```javascript
// Map, filter, reduce
const names = users.map(u => u.name);
const active = users.filter(u => u.active);
const total = items.reduce((sum, i) => sum + i.price, 0);
async function* paginateApi(baseUrl) {
let page = 1;
while (true) {
const data = await fetchJson(`${baseUrl}?page=${page}`);
if (data.items.length === 0) break;
yield* data.items;
page++;
}
}
// Find and includes
const user = users.find(u => u.id === id);
const hasAdmin = users.some(u => u.role === 'admin');
// Consume the async iterator
for await (const item of paginateApi("/api/records")) {
processItem(item);
}
```
### Classes
---
### 3. Closures & Scope
#### Closure Patterns
```javascript
class UserService {
#db; // Private field
// Counter with private state
function createCounter(initial = 0) {
let count = initial;
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count,
reset: () => { count = initial; },
};
}
constructor(database) {
this.#db = database;
const counter = createCounter(10);
counter.increment(); // 11
counter.getCount(); // 11
```
#### Module Pattern (Private State via Closures)
```javascript
const rateLimiter = (() => {
const requests = new Map();
function isAllowed(clientId, maxPerMinute = 60) {
const now = Date.now();
const windowStart = now - 60_000;
const clientRequests = (requests.get(clientId) ?? []).filter(
(t) => t > windowStart
);
if (clientRequests.length >= maxPerMinute) return false;
clientRequests.push(now);
requests.set(clientId, clientRequests);
return true;
}
async findById(id) {
return this.#db.users.find(u => u.id === id);
function reset(clientId) {
requests.delete(clientId);
}
return { isAllowed, reset };
})();
```
#### WeakRef and FinalizationRegistry
```javascript
// Cache that does not prevent garbage collection
const cache = new Map();
function getCached(key, factory) {
const ref = cache.get(key);
const cached = ref?.deref();
if (cached !== undefined) return cached;
const value = factory();
cache.set(key, new WeakRef(value));
return value;
}
// Cleanup when objects are garbage collected
const registry = new FinalizationRegistry((key) => {
cache.delete(key);
});
```
---
### 4. Iteration Protocols
#### Custom Iterator
```javascript
class Range {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
[Symbol.iterator]() {
let current = this.start;
const { end, step } = this;
return {
next() {
if (current < end) {
const value = current;
current += step;
return { value, done: false };
}
return { done: true };
},
};
}
}
for (const n of new Range(0, 10, 2)) {
console.log(n); // 0, 2, 4, 6, 8
}
```
#### Generators
```javascript
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Take first N values
function take(iterable, count) {
const result = [];
for (const value of iterable) {
result.push(value);
if (result.length >= count) break;
}
return result;
}
take(fibonacci(), 8); // [0, 1, 1, 2, 3, 5, 8, 13]
```
#### Lazy Evaluation with Generators
```javascript
function* map(iterable, fn) {
for (const item of iterable) {
yield fn(item);
}
}
function* filter(iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) yield item;
}
}
// Compose lazily -- no intermediate arrays
const data = filter(
map(readLargeFile(), (line) => line.trim()),
(line) => line.length > 0
);
```
#### Async Generators
```javascript
async function* readChunks(reader) {
while (true) {
const { done, value } = await reader.read();
if (done) break;
yield value;
}
}
// Stream processing
const response = await fetch("/api/large-data");
for await (const chunk of readChunks(response.body.getReader())) {
processChunk(chunk);
}
```
---
### 5. Proxy & Reflect
#### Validation Proxy
```javascript
function createValidated(target, validators) {
return new Proxy(target, {
set(obj, prop, value) {
const validate = validators[prop];
if (validate && !validate(value)) {
throw new TypeError(`Invalid value for ${String(prop)}: ${value}`);
}
return Reflect.set(obj, prop, value);
},
});
}
const user = createValidated(
{ name: "", age: 0 },
{
name: (v) => typeof v === "string" && v.length > 0,
age: (v) => typeof v === "number" && v >= 0 && v <= 150,
}
);
user.name = "Alice"; // works
user.age = -1; // throws TypeError
```
#### Observable Object
```javascript
function createObservable(target, onChange) {
return new Proxy(target, {
set(obj, prop, value) {
const oldValue = obj[prop];
const result = Reflect.set(obj, prop, value);
if (oldValue !== value) {
onChange(prop, value, oldValue);
}
return result;
},
deleteProperty(obj, prop) {
const oldValue = obj[prop];
const result = Reflect.deleteProperty(obj, prop);
onChange(prop, undefined, oldValue);
return result;
},
});
}
const state = createObservable({}, (prop, newVal, oldVal) => {
console.log(`${prop}: ${oldVal} -> ${newVal}`);
});
```
#### Property Access Logging
```javascript
function withLogging(target, label = "access") {
return new Proxy(target, {
get(obj, prop) {
console.log(`[${label}] get .${String(prop)}`);
return Reflect.get(obj, prop);
},
has(obj, prop) {
console.log(`[${label}] has .${String(prop)}`);
return Reflect.has(obj, prop);
},
});
}
```
---
### 6. Module System
#### ESM (import/export)
```javascript
// Named exports
export function formatDate(date) { ... }
export const MAX_RETRIES = 3;
// Default export
export default class ApiClient { ... }
// Re-exports
export { formatDate } from "./utils.js";
export { default as ApiClient } from "./api-client.js";
```
#### Dynamic import()
```javascript
// Lazy load modules
async function loadChart(type) {
const module = await import(`./charts/${type}.js`);
return new module.default();
}
// Conditional loading
const { marked } = await import("marked");
// With error handling
async function tryLoadPlugin(name) {
try {
return await import(`./plugins/${name}.js`);
} catch {
console.warn(`Plugin ${name} not available`);
return null;
}
}
```
#### import.meta
```javascript
// Current module URL
console.log(import.meta.url);
// Resolve relative paths (Node.js)
const configPath = new URL("./config.json", import.meta.url);
// Check if file is the entry point (Node.js)
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
// Vite environment variables
const apiUrl = import.meta.env.VITE_API_URL;
```
#### Top-level await
```javascript
// config.js -- top-level await in ESM modules
const response = await fetch("/api/config");
export const config = await response.json();
// db.js
import { createPool } from "./db-pool.js";
export const pool = await createPool(process.env.DATABASE_URL);
```
---
### 7. Performance
#### structuredClone
```javascript
// Deep clone without library (replaces JSON.parse(JSON.stringify(...)))
const original = { nested: { array: [1, 2, 3], date: new Date() } };
const clone = structuredClone(original);
// Handles Date, Map, Set, ArrayBuffer, RegExp (but not functions)
clone.nested.array.push(4);
console.log(original.nested.array.length); // still 3
```
#### requestAnimationFrame
```javascript
// Smooth animation loop
function animate(timestamp) {
updatePosition(timestamp);
render();
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
// Throttle DOM updates to frame rate
let rafId = null;
function scheduleUpdate(data) {
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
applyDOMUpdate(data);
rafId = null;
});
}
```
#### requestIdleCallback
```javascript
// Run low-priority work when the browser is idle
function processQueue(queue) {
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && queue.length > 0) {
const task = queue.shift();
task();
}
if (queue.length > 0) {
processQueue(queue); // schedule remaining
}
});
}
```
#### Web Workers Basics
```javascript
// main.js
const worker = new Worker(new URL("./worker.js", import.meta.url), {
type: "module",
});
worker.postMessage({ data: largeDataSet, operation: "sort" });
worker.onmessage = (event) => {
const sorted = event.data;
renderResults(sorted);
};
// worker.js
self.onmessage = (event) => {
const { data, operation } = event.data;
if (operation === "sort") {
self.postMessage(data.sort((a, b) => a - b));
}
};
```
#### performance.mark / measure
```javascript
// Measure operation duration
performance.mark("fetch-start");
const data = await fetchJson("/api/data");
performance.mark("fetch-end");
performance.measure("fetch-duration", "fetch-start", "fetch-end");
const measurement = performance.getEntriesByName("fetch-duration")[0];
console.log(`Fetch took ${measurement.duration.toFixed(2)}ms`);
// Clean up
performance.clearMarks();
performance.clearMeasures();
```
---
## Best Practices
1. Use `const` by default, `let` when needed
2. Avoid `var` - use block-scoped declarations
3. Use arrow functions for callbacks
4. Handle all promise rejections
5. Use ESLint for consistent style
1. **Use `const` by default, `let` only when reassignment is needed** -- never use `var`. Block scoping prevents entire categories of bugs from hoisting and accidental mutation.
2. **Handle all promise rejections** -- unhandled rejections crash Node.js processes. Always use try/catch with await, or attach `.catch()` to promise chains. Add a global handler as a safety net.
```javascript
process.on("unhandledRejection", (reason) => {
console.error("Unhandled rejection:", reason);
process.exit(1);
});
```
3. **Use arrow functions for callbacks, regular functions for methods** -- arrow functions capture `this` from the enclosing scope, which is correct for callbacks but breaks object methods that need their own `this`.
4. **Prefer `for...of` over `for...in` for iteration** -- `for...in` iterates over all enumerable properties including inherited ones. Use `for...of` for arrays and iterables, `Object.entries()` for objects.
5. **Use ESLint and Prettier** -- enforce consistent style automatically. Configure in the project root and run on pre-commit hooks.
6. **Avoid mutating function arguments** -- create new objects and arrays with spread syntax instead of modifying inputs in place. This prevents action-at-a-distance bugs.
7. **Use `structuredClone` for deep copies** -- replaces the `JSON.parse(JSON.stringify(x))` hack. Handles Dates, Maps, Sets, and circular references correctly.
8. **Use private class fields (`#field`)** -- the `#` prefix creates truly private fields that cannot be accessed outside the class, unlike the `_` convention which is only a hint.
---
## Common Pitfalls
- **Implicit type coercion**: Use `===` instead of `==`
- **Callback hell**: Use async/await
- **Mutating objects**: Create new objects with spread
- **Not handling errors**: Always catch promise rejections
1. **Implicit type coercion** -- always use `===` and `!==`. The `==` operator performs type coercion with surprising rules (`"" == false`, `0 == null` is false but `0 == undefined` is also false, yet `null == undefined` is true).
2. **Forgetting `await`** -- a missing `await` silently returns a Promise object instead of the resolved value, causing hard-to-debug issues.
```javascript
// BAD -- data is a Promise, not the response
const data = fetchJson("/api/data");
// GOOD
const data = await fetchJson("/api/data");
```
3. **`this` binding in callbacks** -- regular functions in callbacks lose their `this` context. Use arrow functions or `.bind()`.
```javascript
// BAD
class Timer {
start() { setTimeout(function() { this.tick(); }, 1000); }
}
// GOOD
class Timer {
start() { setTimeout(() => this.tick(), 1000); }
}
```
4. **Mutating objects passed by reference** -- objects and arrays are passed by reference. Modifying a parameter modifies the original.
```javascript
// BAD
function addDefaults(config) {
config.retries = config.retries ?? 3; // mutates caller's object
return config;
}
// GOOD
function addDefaults(config) {
return { retries: 3, ...config };
}
```
5. **`for...in` on arrays** -- iterates over indices as strings and includes inherited properties. Use `for...of` or array methods.
```javascript
// BAD
for (const i in [10, 20, 30]) {
console.log(typeof i); // "string", not "number"
}
// GOOD
for (const value of [10, 20, 30]) {
console.log(value); // 10, 20, 30
}
```
6. **Floating point arithmetic** -- `0.1 + 0.2 !== 0.3` in JavaScript. For financial calculations, work in integer cents or use a decimal library.
```javascript
// BAD
const total = 0.1 + 0.2; // 0.30000000000000004
// GOOD
const totalCents = 10 + 20; // 30
const total = totalCents / 100; // 0.3
```
---
## 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
@@ -0,0 +1,247 @@
# 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) |
+640 -50
View File
@@ -1,9 +1,11 @@
---
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.
---
# Python
## Description
Python development expertise including type hints, async patterns, virtual environments, and Pythonic idioms.
## When to Use
- Working with Python files (.py)
@@ -11,59 +13,148 @@ Python development expertise including type hints, async patterns, virtual envir
- Using Python frameworks (Django, FastAPI, Flask)
- Data processing and automation
## When NOT to Use
- JavaScript or TypeScript-only projects with no Python components
- Non-Python environments where another language skill is more appropriate
---
## Core Patterns
### Type Hints
### 1. Type Hints
Use type hints on all public functions and module-level variables. Python 3.10+ syntax is preferred (use `X | Y` instead of `Union[X, Y]`).
#### Basic Types
```python
from typing import Optional, List, Dict, Union
from collections.abc import Callable
from typing import Any
def process_items(
items: List[str],
callback: Callable[[str], None],
config: Optional[Dict[str, Any]] = None
) -> List[str]:
"""Process items with optional callback."""
return [callback(item) for item in items]
def greet(name: str) -> str:
return f"Hello, {name}"
def process(count: int, factor: float = 1.0) -> float:
return count * factor
def is_valid(data: bytes | None) -> bool:
return data is not None and len(data) > 0
```
### Async/Await
#### Optional and Union
```python
import asyncio
from typing import List
# Python 3.10+ syntax (preferred)
def find_user(user_id: int) -> User | None:
...
async def fetch_data(url: str) -> dict:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.json()
# Pre-3.10 fallback
from typing import Optional, Union
async def fetch_all(urls: List[str]) -> List[dict]:
return await asyncio.gather(*[fetch_data(url) for url in urls])
def find_user(user_id: int) -> Optional[User]:
...
def parse_input(value: Union[str, int]) -> str:
return str(value)
```
### Context Managers
#### Generic Collections
```python
from contextlib import contextmanager
# Python 3.9+ built-in generics (preferred)
def process_items(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}
@contextmanager
def managed_resource():
resource = acquire_resource()
try:
yield resource
finally:
release_resource(resource)
def merge_configs(base: dict[str, Any], overrides: dict[str, Any]) -> dict[str, Any]:
return {**base, **overrides}
# Usage
with managed_resource() as r:
r.do_something()
# Nested generics
def group_by_key(pairs: list[tuple[str, int]]) -> dict[str, list[int]]:
result: dict[str, list[int]] = {}
for key, value in pairs:
result.setdefault(key, []).append(value)
return result
```
### Dataclasses
#### Protocol for Structural Subtyping
```python
from typing import Protocol, runtime_checkable
@runtime_checkable
class Renderable(Protocol):
def render(self) -> str: ...
class HtmlWidget:
def render(self) -> str:
return "<div>widget</div>"
def display(item: Renderable) -> None:
print(item.render())
# HtmlWidget satisfies Renderable without inheriting from it
display(HtmlWidget()) # works
```
#### TypeVar for Generic Functions
```python
from typing import TypeVar, Sequence
T = TypeVar("T")
def first(items: Sequence[T]) -> T:
return items[0]
# Bounded TypeVar
Numeric = TypeVar("Numeric", int, float)
def clamp(value: Numeric, low: Numeric, high: Numeric) -> Numeric:
return max(low, min(high, value))
```
#### @overload for Multiple Signatures
```python
from typing import overload
@overload
def parse(raw: str) -> dict[str, Any]: ...
@overload
def parse(raw: bytes) -> dict[str, Any]: ...
@overload
def parse(raw: str, as_list: bool) -> list[Any]: ...
def parse(raw: str | bytes, as_list: bool = False) -> dict[str, Any] | list[Any]:
data = raw if isinstance(raw, str) else raw.decode()
parsed = json.loads(data)
return list(parsed) if as_list else parsed
```
#### TypeAlias and TypeGuard
```python
from typing import TypeAlias, TypeGuard
# TypeAlias for complex types
JsonValue: TypeAlias = str | int | float | bool | None | list["JsonValue"] | dict[str, "JsonValue"]
Headers: TypeAlias = dict[str, str]
# TypeGuard for narrowing
def is_string_list(val: list[Any]) -> TypeGuard[list[str]]:
return all(isinstance(item, str) for item in val)
def process(items: list[Any]) -> None:
if is_string_list(items):
# items is now list[str] inside this branch
print(", ".join(items))
```
---
### 2. Dataclasses & Pydantic
#### @dataclass with Options
```python
from dataclasses import dataclass, field
@@ -75,36 +166,535 @@ class User:
email: str
name: str
created_at: datetime = field(default_factory=datetime.now)
tags: list[str] = field(default_factory=list)
def __post_init__(self):
self.email = self.email.lower()
self.email = self.email.strip().lower()
```
### Pydantic Models
#### Frozen and Slots
```python
from pydantic import BaseModel, EmailStr, Field
@dataclass(frozen=True, slots=True)
class Coordinate:
"""Immutable, memory-efficient value object."""
x: float
y: float
@property
def magnitude(self) -> float:
return (self.x ** 2 + self.y ** 2) ** 0.5
```
#### Pydantic BaseModel
```python
from pydantic import BaseModel, EmailStr, Field, field_validator, computed_field, model_validator
class UserCreate(BaseModel):
model_config = {"str_strip_whitespace": True, "frozen": False}
email: EmailStr
name: str = Field(min_length=1, max_length=100)
password: str = Field(min_length=8)
age: int = Field(ge=0, le=150)
class Config:
str_strip_whitespace = True
@field_validator("name")
@classmethod
def name_must_not_be_blank(cls, v: str) -> str:
if not v.strip():
raise ValueError("Name must not be blank")
return v.title()
@computed_field
@property
def display_name(self) -> str:
return f"{self.name} <{self.email}>"
@model_validator(mode="after")
def check_consistency(self) -> "UserCreate":
if "admin" in self.name.lower() and self.age < 18:
raise ValueError("Admins must be 18+")
return self
```
---
### 3. Async Patterns
#### Basic async/await
```python
import asyncio
import aiohttp
async def fetch_json(url: str) -> dict:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
response.raise_for_status()
return await response.json()
```
#### asyncio.gather for Parallel Work
```python
async def fetch_all(urls: list[str]) -> list[dict]:
return await asyncio.gather(*[fetch_json(url) for url in urls])
```
#### asyncio.TaskGroup (Python 3.11+)
```python
async def fetch_all_safe(urls: list[str]) -> list[dict]:
results: list[dict] = []
async with asyncio.TaskGroup() as tg:
for url in urls:
tg.create_task(fetch_and_append(url, results))
return results
async def fetch_and_append(url: str, results: list[dict]) -> None:
data = await fetch_json(url)
results.append(data)
```
#### Async Generators
```python
async def paginate(url: str) -> AsyncIterator[dict]:
page = 1
while True:
data = await fetch_json(f"{url}?page={page}")
if not data["items"]:
break
for item in data["items"]:
yield item
page += 1
# Usage
async for item in paginate("/api/users"):
process(item)
```
#### Async Context Managers
```python
from contextlib import asynccontextmanager
@asynccontextmanager
async def db_transaction(pool):
conn = await pool.acquire()
tx = await conn.begin()
try:
yield conn
await tx.commit()
except Exception:
await tx.rollback()
raise
finally:
await pool.release(conn)
```
#### Semaphores for Concurrency Limiting
```python
async def fetch_with_limit(urls: list[str], max_concurrent: int = 10) -> list[dict]:
semaphore = asyncio.Semaphore(max_concurrent)
async def limited_fetch(url: str) -> dict:
async with semaphore:
return await fetch_json(url)
return await asyncio.gather(*[limited_fetch(url) for url in urls])
```
---
### 4. Decorators
#### Function Decorator with functools.wraps
```python
import functools
import time
def timing(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timing
def slow_operation():
time.sleep(1)
```
#### Decorator with Arguments
```python
def retry(max_attempts: int = 3, delay: float = 1.0):
def decorator(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
last_error: Exception | None = None
for attempt in range(max_attempts):
try:
return await func(*args, **kwargs)
except Exception as e:
last_error = e
if attempt < max_attempts - 1:
await asyncio.sleep(delay * (2 ** attempt))
raise last_error
return wrapper
return decorator
@retry(max_attempts=5, delay=0.5)
async def unreliable_call(url: str) -> dict:
return await fetch_json(url)
```
#### Class Decorator
```python
def singleton(cls):
instances: dict[type, Any] = {}
@functools.wraps(cls)
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class AppConfig:
def __init__(self):
self.settings = load_settings()
```
#### Caching Decorator
```python
from functools import lru_cache, cache
@lru_cache(maxsize=256)
def fibonacci(n: int) -> int:
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# Python 3.9+ unbounded cache
@cache
def load_config(path: str) -> dict:
with open(path) as f:
return json.load(f)
```
---
### 5. Context Managers
#### Basic @contextmanager
```python
from contextlib import contextmanager
@contextmanager
def managed_connection(dsn: str):
conn = connect(dsn)
try:
yield conn
finally:
conn.close()
with managed_connection("postgres://...") as conn:
conn.execute("SELECT 1")
```
#### Temporary File Context Manager
```python
import tempfile
import os
@contextmanager
def temp_directory():
dirpath = tempfile.mkdtemp()
try:
yield dirpath
finally:
shutil.rmtree(dirpath)
with temp_directory() as tmpdir:
filepath = os.path.join(tmpdir, "data.json")
write_json(filepath, data)
```
#### Lock Context Manager
```python
import threading
@contextmanager
def timed_lock(lock: threading.Lock, timeout: float = 5.0):
acquired = lock.acquire(timeout=timeout)
if not acquired:
raise TimeoutError("Could not acquire lock")
try:
yield
finally:
lock.release()
```
#### Async Context Manager
```python
from contextlib import asynccontextmanager
@asynccontextmanager
async def http_session():
session = aiohttp.ClientSession()
try:
yield session
finally:
await session.close()
```
---
### 6. Pattern Matching
#### Basic match/case
```python
def handle_command(command: str) -> str:
match command.split():
case ["quit"]:
return "Goodbye"
case ["hello", name]:
return f"Hello, {name}"
case ["add", *items]:
return f"Adding {len(items)} items"
case _:
return "Unknown command"
```
#### Structural Patterns
```python
def process_event(event: dict) -> None:
match event:
case {"type": "click", "x": int(x), "y": int(y)}:
handle_click(x, y)
case {"type": "keypress", "key": str(key)} if len(key) == 1:
handle_keypress(key)
case {"type": "resize", "width": w, "height": h}:
handle_resize(w, h)
```
#### Guard Clauses and OR Patterns
```python
def classify_status(code: int) -> str:
match code:
case 200 | 201 | 204:
return "success"
case code if 300 <= code < 400:
return "redirect"
case 401 | 403:
return "auth_error"
case code if 400 <= code < 500:
return "client_error"
case code if 500 <= code < 600:
return "server_error"
case _:
return "unknown"
```
---
### 7. Error Handling
#### Custom Exception Hierarchies
```python
class AppError(Exception):
"""Base exception for the application."""
def __init__(self, message: str, code: str | None = None):
super().__init__(message)
self.code = code
class NotFoundError(AppError):
"""Resource was not found."""
def __init__(self, resource: str, resource_id: str):
super().__init__(f"{resource} {resource_id} not found", code="NOT_FOUND")
self.resource = resource
self.resource_id = resource_id
class ValidationError(AppError):
"""Input validation failed."""
def __init__(self, errors: list[str]):
super().__init__(f"Validation failed: {'; '.join(errors)}", code="VALIDATION")
self.errors = errors
```
#### ExceptionGroup (Python 3.11+)
```python
async def process_batch(items: list[dict]) -> list[dict]:
results = []
errors = []
for item in items:
try:
results.append(await process(item))
except Exception as e:
errors.append(e)
if errors:
raise ExceptionGroup("Batch processing errors", errors)
return results
# Handling with except*
try:
await process_batch(items)
except* ValueError as eg:
print(f"Validation errors: {len(eg.exceptions)}")
except* ConnectionError as eg:
print(f"Connection errors: {len(eg.exceptions)}")
```
#### Exception Chaining
```python
def load_config(path: str) -> dict:
try:
with open(path) as f:
return json.load(f)
except FileNotFoundError as e:
raise AppError(f"Config file missing: {path}") from e
except json.JSONDecodeError as e:
raise AppError(f"Invalid JSON in {path}") from e
```
#### contextlib.suppress
```python
from contextlib import suppress
# Instead of try/except/pass
with suppress(FileNotFoundError):
os.remove("temp_file.txt")
# Instead of:
# try:
# os.remove("temp_file.txt")
# except FileNotFoundError:
# pass
```
---
## Best Practices
1. Use type hints for all public functions
2. Use dataclasses or Pydantic for data models
3. Prefer context managers for resource management
4. Use async for I/O-bound operations
5. Follow PEP 8 style guidelines
1. **Use type hints on all public functions** -- they serve as documentation, enable IDE autocompletion, and allow static analysis with mypy or pyright.
2. **Prefer dataclasses or Pydantic for structured data** -- avoid passing raw dicts around. Use `@dataclass` for internal data, Pydantic `BaseModel` for external boundaries (API input/output, config files).
3. **Use context managers for resource management** -- database connections, file handles, locks, and temporary resources should always be wrapped in `with` statements to guarantee cleanup.
4. **Prefer `asyncio.TaskGroup` over bare `gather`** -- TaskGroup (3.11+) provides proper error handling by cancelling sibling tasks when one fails, avoiding orphaned coroutines.
5. **Follow PEP 8 and use a formatter** -- use `ruff format` or `black` for consistent formatting, and `ruff check` for linting. Configure in `pyproject.toml`.
6. **Write small, composable functions** -- each function should do one thing. Prefer returning values over mutating state. Limit functions to ~20 lines when practical.
7. **Use `__all__` in public modules** -- explicitly declare the public API of a module to prevent accidental imports of internal helpers.
8. **Use `pathlib.Path` over `os.path`** -- pathlib provides a cleaner, object-oriented API for file system operations and works cross-platform.
---
## Common Pitfalls
- **Mutable default arguments**: Use `None` and initialize in function
- **Not closing resources**: Use `with` statements
- **Blocking in async**: Use `asyncio.to_thread()` for CPU work
- **Catching bare exceptions**: Be specific with exception types
1. **Mutable default arguments** -- default values are shared across calls. Use `None` and initialize inside the function body.
```python
# BAD
def add_item(item: str, items: list[str] = []) -> list[str]: ...
# GOOD
def add_item(item: str, items: list[str] | None = None) -> list[str]:
if items is None:
items = []
items.append(item)
return items
```
2. **Blocking calls inside async functions** -- calling `time.sleep()`, `requests.get()`, or CPU-heavy code in an async function blocks the entire event loop. Use `asyncio.to_thread()` or `asyncio.sleep()`.
```python
# BAD
async def fetch():
return requests.get(url) # blocks event loop
# GOOD
async def fetch():
return await asyncio.to_thread(requests.get, url)
```
3. **Catching bare `Exception`** -- always be specific about which exceptions you catch. Bare `except:` or `except Exception:` hides bugs.
```python
# BAD
try:
result = compute()
except Exception:
pass
# GOOD
try:
result = compute()
except (ValueError, TypeError) as e:
logger.warning("Computation failed: %s", e)
result = default_value
```
4. **Using `is` for value comparison** -- `is` checks identity, not equality. Only use `is` for `None`, `True`, `False`, and sentinel objects.
```python
# BAD
if x is 42: ...
# GOOD
if x == 42: ...
if x is None: ...
```
5. **Forgetting to close resources** -- file handles, database connections, and HTTP sessions leak if not closed. Always use context managers.
```python
# BAD
f = open("data.txt")
data = f.read()
# GOOD
with open("data.txt") as f:
data = f.read()
```
6. **Circular imports** -- restructure code to avoid circular dependencies. Move shared types into a separate module, use `TYPE_CHECKING` for type-only imports, or use lazy imports.
```python
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from myapp.models import User # only imported during type checking
```
---
## 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
@@ -0,0 +1,216 @@
# 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) |
+625 -60
View File
@@ -1,9 +1,11 @@
---
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.
---
# TypeScript
## Description
TypeScript development with strict typing, advanced type utilities, and modern patterns.
## When to Use
- Working with TypeScript files (.ts, .tsx)
@@ -11,50 +13,284 @@ TypeScript development with strict typing, advanced type utilities, and modern p
- React/Next.js development
- Node.js backend development
## When NOT to Use
- Pure Python projects with no TypeScript components
- JavaScript projects that have no TypeScript setup and are not being migrated to TypeScript
---
## Core Patterns
### Type Definitions
### 1. Advanced Types
#### Discriminated Unions
```typescript
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}
```
#### Branded Types
```typescript
type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };
function createUserId(id: string): UserId {
if (!id.startsWith("usr_")) throw new Error("Invalid user ID");
return id as UserId;
}
function getUser(id: UserId): User { ... }
// Prevents mixing IDs:
const userId = createUserId("usr_123");
const orderId = "ord_456" as OrderId;
// getUser(orderId); // compile error
```
#### Template Literal Types
```typescript
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiRoute = `/api/${string}`;
type EventName = `on${Capitalize<string>}`;
// Combine for precise route definitions
type Endpoint = `${Uppercase<HttpMethod>} ${ApiRoute}`;
// Pattern matching on 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/:userId/posts/:postId">;
// Result: "userId" | "postId"
```
#### Conditional Types with infer
```typescript
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type UnwrapArray<T> = T extends (infer U)[] ? U : T;
// Deeply unwrap nested promises
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
// Extract function return type conditionally
type AsyncReturnType<T extends (...args: any[]) => any> =
ReturnType<T> extends Promise<infer U> ? U : ReturnType<T>;
```
#### Mapped Types
```typescript
// Make all properties optional and nullable
type Nullable<T> = { [K in keyof T]: T[K] | null };
// Create a readonly version with getters
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
// Remove specific keys
type RemoveKind<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K];
};
```
#### Recursive Types
```typescript
type Json =
| string
| number
| boolean
| null
| Json[]
| { [key: string]: Json };
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
```
---
### 2. Utility Types
```typescript
// Interfaces for objects
interface User {
id: string;
email: string;
name: string;
role: "admin" | "user" | "guest";
createdAt: Date;
}
// Types for unions and utilities
type Status = 'pending' | 'active' | 'inactive';
type UserWithStatus = User & { status: Status };
// Generic types
type ApiResponse<T> = {
data: T;
error?: string;
status: number;
};
```
### Utility Types
```typescript
// Partial - all properties optional
// Partial -- all properties optional (useful for update payloads)
type UserUpdate = Partial<User>;
// Pick - select properties
type UserPreview = Pick<User, 'id' | 'name'>;
// Required -- all properties required (undo optionality)
type CompleteUser = Required<User>;
// Omit - exclude properties
type UserWithoutId = Omit<User, 'id'>;
// Pick -- select specific properties
type UserPreview = Pick<User, "id" | "name">;
// Record - dictionary type
type UserMap = Record<string, User>;
// Omit -- exclude specific properties
type UserCreate = Omit<User, "id" | "createdAt">;
// Record -- dictionary with typed keys and values
type RolePermissions = Record<User["role"], string[]>;
// Exclude -- remove members from a union
type NonGuestRole = Exclude<User["role"], "guest">;
// Result: "admin" | "user"
// Extract -- keep only matching members from a union
type PrivilegedRole = Extract<User["role"], "admin" | "moderator">;
// Result: "admin"
// ReturnType -- extract return type of a function
declare function getUser(): Promise<User>;
type GetUserResult = ReturnType<typeof getUser>;
// Result: Promise<User>
// Parameters -- extract parameter types as a tuple
type GetUserParams = Parameters<typeof getUser>;
// Awaited -- unwrap Promise types
type ResolvedUser = Awaited<ReturnType<typeof getUser>>;
// Result: User
// NonNullable -- remove null and undefined
type DefinitelyString = NonNullable<string | null | undefined>;
// Result: string
```
### Async Patterns
---
### 3. Generics
#### Generic Functions
```typescript
function first<T>(items: T[]): T | undefined {
return items[0];
}
function groupBy<T, K extends string | number>(
items: T[],
keyFn: (item: T) => K,
): Record<K, T[]> {
const result = {} as Record<K, T[]>;
for (const item of items) {
const key = keyFn(item);
(result[key] ??= []).push(item);
}
return result;
}
```
#### Generic Constraints with extends
```typescript
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find((item) => item.id === id);
}
// Multiple constraints
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
```
#### Generic Classes
```typescript
class Repository<T extends HasId> {
private items = new Map<string, T>();
save(item: T): void {
this.items.set(item.id, item);
}
findById(id: string): T | undefined {
return this.items.get(id);
}
findAll(): T[] {
return [...this.items.values()];
}
}
const userRepo = new Repository<User>();
```
#### Default Type Parameters
```typescript
type ApiResponse<T, E = Error> = {
data: T | null;
error: E | null;
status: number;
};
// Uses default Error type
const response: ApiResponse<User> = {
data: user,
error: null,
status: 200,
};
// Override with custom error
const response2: ApiResponse<User, ValidationError> = { ... };
```
#### const Type Parameters (TypeScript 5.0+)
```typescript
function createRoute<const T extends readonly string[]>(
methods: T,
path: string,
) {
return { methods, path };
}
// Infers literal tuple type ["GET", "POST"] instead of string[]
const route = createRoute(["GET", "POST"], "/api/users");
```
---
### 4. Async Patterns
#### Promise Typing
```typescript
async function fetchUser(id: string): Promise<User> {
@@ -62,67 +298,396 @@ async function fetchUser(id: string): Promise<User> {
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
}
return response.json();
return response.json() as Promise<User>;
}
```
// Error handling
async function safeOperation<T>(
operation: () => Promise<T>
): Promise<[T, null] | [null, Error]> {
#### Promise.all with Tuple Types
```typescript
async function loadDashboard(userId: string) {
const [user, posts, notifications] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchNotifications(userId),
] as const);
// user: User, posts: Post[], notifications: Notification[]
return { user, posts, notifications };
}
```
#### Result Pattern for Error Handling
```typescript
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
async function safeAsync<T>(
fn: () => Promise<T>,
): Promise<Result<T>> {
try {
const result = await operation();
return [result, null];
return { ok: true, value: await fn() };
} catch (error) {
return [null, error as Error];
return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };
}
}
const result = await safeAsync(() => fetchUser("123"));
if (result.ok) {
console.log(result.value.name);
} else {
console.error(result.error.message);
}
```
### Class Patterns
#### AbortController Patterns
```typescript
class UserService {
constructor(private readonly db: Database) {}
async function fetchWithTimeout(
url: string,
timeoutMs: number = 5000,
): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
async findById(id: string): Promise<User | null> {
return this.db.users.findUnique({ where: { id } });
try {
return await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timeoutId);
}
}
async create(data: UserCreate): Promise<User> {
return this.db.users.create({ data });
}
// Cancellable operation
function createCancellableRequest(url: string) {
const controller = new AbortController();
const promise = fetch(url, { signal: controller.signal });
return {
promise,
cancel: () => controller.abort(),
};
}
```
### Zod Validation
---
### 5. Zod Integration
#### Schema Definition and Inference
```typescript
import { z } from 'zod';
import { z } from "zod";
const UserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
password: z.string().min(8),
age: z.number().int().min(0).max(150),
role: z.enum(["admin", "user", "guest"]),
});
type UserInput = z.infer<typeof UserSchema>;
type User = z.infer<typeof UserSchema>;
```
function validateUser(data: unknown): UserInput {
return UserSchema.parse(data);
#### Refinements and Transforms
```typescript
const PasswordSchema = z
.string()
.min(8)
.refine((val) => /[A-Z]/.test(val), "Must contain uppercase")
.refine((val) => /[0-9]/.test(val), "Must contain number");
const DateStringSchema = z
.string()
.transform((val) => new Date(val))
.refine((date) => !isNaN(date.getTime()), "Invalid date");
const MoneySchema = z
.string()
.transform((val) => parseFloat(val.replace(/[$,]/g, "")))
.pipe(z.number().positive());
```
#### Discriminated Unions with Zod
```typescript
const ShapeSchema = z.discriminatedUnion("kind", [
z.object({ kind: z.literal("circle"), radius: z.number().positive() }),
z.object({ kind: z.literal("rectangle"), width: z.number(), height: z.number() }),
]);
type Shape = z.infer<typeof ShapeSchema>;
function validateShape(input: unknown): Shape {
return ShapeSchema.parse(input);
}
```
#### Zod with API Validation
```typescript
const QueryParamsSchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
search: z.string().optional(),
sort: z.enum(["name", "date", "relevance"]).default("date"),
});
type QueryParams = z.infer<typeof QueryParamsSchema>;
function parseQuery(raw: Record<string, string>): QueryParams {
return QueryParamsSchema.parse(raw);
}
```
---
### 6. Module Patterns
#### Barrel Exports
```typescript
// src/models/index.ts
export { User, type UserCreate } from "./user.js";
export { Post, type PostCreate } from "./post.js";
export { Comment } from "./comment.js";
```
#### Declaration Merging
```typescript
// Extend an existing interface
interface Window {
analytics: AnalyticsClient;
}
// Extend Express Request
declare namespace Express {
interface Request {
user?: AuthenticatedUser;
}
}
```
#### Module Augmentation
```typescript
// Augment a third-party module
import "express";
declare module "express" {
interface Request {
requestId: string;
startTime: number;
}
}
```
#### Ambient Declarations (.d.ts)
```typescript
// global.d.ts
declare global {
interface ImportMeta {
env: {
VITE_API_URL: string;
VITE_APP_TITLE: string;
};
}
}
// Declare untyped modules
declare module "legacy-lib" {
export function doSomething(input: string): string;
}
export {};
```
---
### 7. Type Guards
#### Built-in Narrowing
```typescript
function process(value: string | number | null) {
if (typeof value === "string") {
// value: string
return value.toUpperCase();
}
if (typeof value === "number") {
// value: number
return value.toFixed(2);
}
// value: null
return "N/A";
}
```
#### instanceof Guard
```typescript
class ApiError extends Error {
constructor(
message: string,
public statusCode: number,
) {
super(message);
}
}
function handleError(error: unknown): string {
if (error instanceof ApiError) {
return `API Error ${error.statusCode}: ${error.message}`;
}
if (error instanceof Error) {
return error.message;
}
return String(error);
}
```
#### in Operator Guard
```typescript
interface Dog { bark(): void; breed: string; }
interface Cat { meow(): void; color: string; }
function speak(animal: Dog | Cat): void {
if ("bark" in animal) {
animal.bark(); // animal: Dog
} else {
animal.meow(); // animal: Cat
}
}
```
#### Custom Type Predicates (is)
```typescript
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"email" in value &&
typeof (value as User).id === "string"
);
}
function processInput(data: unknown) {
if (isUser(data)) {
// data: User -- fully narrowed
console.log(data.email);
}
}
```
#### Assertion Functions (asserts)
```typescript
function assertDefined<T>(
value: T | null | undefined,
message?: string,
): asserts value is T {
if (value === null || value === undefined) {
throw new Error(message ?? "Value is null or undefined");
}
}
function processUser(maybeUser: User | null): string {
assertDefined(maybeUser, "User is required");
// maybeUser: User after this point
return maybeUser.name;
}
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
// Exhaustiveness checking in switch
function getLabel(role: "admin" | "user" | "guest"): string {
switch (role) {
case "admin": return "Administrator";
case "user": return "Standard User";
case "guest": return "Guest";
default: return assertNever(role); // compile error if a case is missed
}
}
```
---
## Best Practices
1. Enable strict mode in tsconfig.json
2. Avoid `any` - use `unknown` and type guards
3. Use interfaces for object shapes, types for unions
4. Prefer `const` assertions for literal types
5. Use discriminated unions for state
1. **Enable strict mode in tsconfig.json** -- set `"strict": true` which enables `strictNullChecks`, `noImplicitAny`, `strictFunctionTypes`, and other safety checks.
2. **Never use `any` -- use `unknown` instead** -- when the type is truly unknown, use `unknown` and narrow with type guards. Reserve `any` only for exceptional migration scenarios, and flag it with `// eslint-disable-next-line`.
3. **Use interfaces for object shapes, types for unions** -- interfaces support declaration merging and produce clearer error messages. Types are better for unions, intersections, and mapped types.
4. **Prefer discriminated unions for state modeling** -- use a shared literal `kind` or `type` field to enable exhaustive switch statements and precise narrowing.
5. **Use `as const` for literal inference** -- `const assertions` preserve literal types in arrays and objects, avoiding unwanted widening to `string[]` or `number[]`.
6. **Validate external data at boundaries** -- use Zod or a similar runtime validator at API boundaries, config loading, and form inputs. Never trust `as` casts for unknown data.
7. **Prefer type predicates over type assertions** -- custom `is` guards are safer than `as` casts because they include a runtime check.
8. **Use `satisfies` for type checking without widening** -- the `satisfies` operator (TS 5.0+) validates that a value conforms to a type while preserving the narrower inferred type.
```typescript
const config = {
apiUrl: "https://api.example.com",
retries: 3,
} satisfies Record<string, string | number>;
// config.apiUrl is still string (not string | number)
```
---
## Common Pitfalls
- **Using `any`**: Defeats type safety
- **Not handling null/undefined**: Use strict null checks
- **Type assertions**: Prefer type guards
- **Ignoring errors**: Handle all promise rejections
1. **Overusing type assertions (`as`)** -- assertions bypass the type checker. Use type guards or schema validation instead.
```typescript
// BAD
const user = data as User;
// GOOD
if (isUser(data)) { ... }
```
2. **Ignoring strict null checks** -- `undefined` and `null` cause runtime crashes when not handled. Always enable `strictNullChecks` and handle nullable values explicitly.
3. **Returning `any` from catch blocks** -- `catch (e)` gives `unknown` in strict mode. Always narrow before using the error.
```typescript
catch (error) {
const message = error instanceof Error ? error.message : String(error);
}
```
4. **Mutation of readonly types at runtime** -- `Readonly<T>` and `readonly` only prevent mutation at compile time. The underlying object can still be mutated at runtime via `Object.assign` or casts.
5. **Forgetting `export {}` in ambient files** -- `.d.ts` files without any import/export are treated as global scripts rather than modules, which can cause unexpected declaration collisions.
6. **Using enums instead of const objects** -- TypeScript enums have quirks (reverse mappings, tree-shaking issues). Prefer `as const` objects or union types.
```typescript
// Prefer this:
const Role = { Admin: "admin", User: "user" } as const;
type Role = (typeof Role)[keyof typeof Role];
// Over this:
enum Role { Admin = "admin", User = "user" }
```
---
## 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
@@ -0,0 +1,249 @@
# 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 { /* ... */ }
}
```
@@ -1,9 +1,11 @@
---
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.
---
# Brainstorming
## Description
Interactive design refinement methodology for turning rough ideas into fully-formed designs through collaborative dialogue. Use this skill during creative development phases before implementation begins.
## When to Use
- Designing new features with unclear requirements
@@ -14,9 +16,9 @@ Interactive design refinement methodology for turning rough ideas into fully-for
## When NOT to Use
- Clear "mechanical" processes with known implementation
- Simple bug fixes with obvious solutions
- Tasks with explicit requirements already defined
- Executing already-approved plans -- use `executing-plans` instead
- Simple bug fixes with obvious solutions -- jump straight to fixing
- Mechanical refactoring where the approach is already clear
---
@@ -188,3 +190,8 @@ For informed technology choices:
```
---
## 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
@@ -0,0 +1,88 @@
# Brainstorming Question Patterns
Quick-reference catalog of effective question types for brainstorming sessions. Use these to systematically explore a problem space before jumping to solutions.
---
## Clarifying Questions
**Purpose:** Ensure you understand the actual problem before solving it. Most failed implementations stem from unclear requirements.
**When to use:** At the start of every brainstorming session, and whenever the request contains ambiguous terms.
| # | Question | Context |
|---|----------|---------|
| 1 | What exactly should happen when a user does X? | Use when the described behavior has multiple valid interpretations. Forces concrete scenario thinking. |
| 2 | Who is the primary user of this feature, and what's their current workflow? | Use when the requester assumes you know the audience. Different users need different solutions. |
| 3 | What does success look like? How will you know this is working? | Use to surface acceptance criteria early. Prevents building the wrong thing correctly. |
| 4 | Can you walk me through a specific example from start to finish? | Use when the description is abstract. Concrete examples reveal hidden requirements. |
---
## Constraint Questions
**Purpose:** Identify boundaries that shape the solution space. Constraints eliminate options early and prevent wasted effort.
**When to use:** After clarifying the goal, before exploring solutions. Especially important when the requester says "just build X."
| # | Question | Context |
|---|----------|---------|
| 1 | What's the timeline? Is there a hard deadline or a target? | Use always. A 2-day solution looks nothing like a 2-month solution. |
| 2 | What can't change? Are there existing systems, APIs, or schemas we must preserve? | Use when modifying an existing system. Reveals integration constraints. |
| 3 | What's the performance budget? Expected load, response time, data volume? | Use for any feature touching data pipelines, APIs, or user-facing flows. |
| 4 | Are there compliance, security, or accessibility requirements? | Use for anything involving user data, payments, or public-facing UI. Easy to forget, expensive to retrofit. |
---
## Alternative Questions
**Purpose:** Expand the solution space. The first idea is rarely the best idea.
**When to use:** After constraints are clear but before committing to an approach. Especially when the requester has already proposed a specific solution.
| # | Question | Context |
|---|----------|---------|
| 1 | What if we solved this without building anything new? Could an existing tool or configuration handle it? | Use to challenge the assumption that code is needed. Sometimes a config change or third-party tool is enough. |
| 2 | What's the simplest version that still delivers value? | Use to find the MVP. Strips away nice-to-haves and focuses on the core need. |
| 3 | Have you considered [opposite approach]? What would that look like? | Use to break anchoring bias. If they propose a push model, ask about pull. If sync, ask about async. |
| 4 | What would we do if we had to ship this today? | Use to identify which parts are truly essential vs. which are aspirational. |
---
## Prioritization Questions
**Purpose:** Sequence work effectively when there's more to do than time allows.
**When to use:** When the feature has multiple components, when scope is growing, or when the team is debating what to build first.
| # | Question | Context |
|---|----------|---------|
| 1 | Which of these capabilities is most important to the first user? | Use to rank features by user impact rather than technical convenience. |
| 2 | What's the MVP — the smallest thing we can ship and learn from? | Use when scope is expanding. Forces a shippable first increment. |
| 3 | What can wait for v2 without blocking the core experience? | Use to defer non-essential work explicitly rather than letting it creep in. |
| 4 | If we could only ship one of these this week, which one? | Use when the team can't agree on priority. Forces a direct comparison. |
---
## Technical Questions
**Purpose:** Ground the discussion in implementation reality. Surface architecture decisions that affect the solution.
**When to use:** Once the goal and constraints are clear, before writing a plan. Essential for features that touch multiple systems.
| # | Question | Context |
|---|----------|---------|
| 1 | What's the data model? What entities exist, and how do they relate? | Use for any feature involving persistent state. Data model drives everything. |
| 2 | How does authentication and authorization work for this? Who can see/do what? | Use for any feature with access control. Auth is often assumed but rarely specified. |
| 3 | What's the expected scale — users, requests/sec, data size? | Use to choose between simple and scalable approaches. Over-engineering is as wasteful as under-engineering. |
| 4 | What existing code or patterns should this follow? Are there conventions to match? | Use to maintain consistency. New code that ignores existing patterns creates maintenance burden. |
---
## Using This Reference
1. **Don't ask all questions** — pick the 3-5 most relevant for the situation
2. **Start with clarifying** — always ensure you understand the problem
3. **Adapt the phrasing** — these are templates, not scripts
4. **Listen for gaps** — the questions the requester struggles to answer reveal the areas that need more thought
5. **Document answers** — capture decisions as they're made so you don't re-ask later
@@ -1,9 +1,11 @@
---
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.
---
# Defense-in-Depth
## Description
Multi-layer validation strategy that makes bugs structurally impossible rather than merely fixed. After finding and fixing a bug's root cause, implement validation at every layer data passes through.
## When to Use
- After fixing any data-related bug
@@ -12,6 +14,12 @@ Multi-layer validation strategy that makes bugs structurally impossible rather t
- Building robust systems
- When single validation points have failed
## When NOT to Use
- Greenfield prototyping where speed matters more than robustness and requirements are still fluid
- Non-data-related bugs such as logic errors, race conditions, or algorithmic mistakes
- UI styling issues where visual correctness is the concern, not data integrity
---
## Core Concept
@@ -283,3 +291,9 @@ After fixing any bug:
- [ ] Bug is structurally impossible, not just fixed
---
## 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
@@ -0,0 +1,197 @@
# Validation Layers Reference
Multi-layer validation strategy ensuring no single point of failure.
## Overview
```
Request -> [Layer 1: Input] -> [Layer 2: Business] -> [Layer 3: Persistence] -> [Layer 4: Output] -> Response
```
Each layer validates independently. A failure at any layer should produce a clear, actionable error. Never rely on a single layer.
## Layer 1: Input Boundary
**Purpose**: Reject malformed, oversized, or obviously invalid data at the edge.
### What to Validate
- Data types and shapes (string, number, object structure)
- Required vs optional fields
- String length, numeric ranges, allowed values
- Format patterns (email, URL, UUID, date)
- Content-Type headers, encoding
- File upload size and MIME type
- Request rate and authentication tokens
### Python (FastAPI + Pydantic)
```python
from pydantic import BaseModel, Field, EmailStr
from fastapi import FastAPI, Query
class CreateUserRequest(BaseModel):
email: EmailStr
name: str = Field(min_length=1, max_length=200)
age: int = Field(ge=0, le=150)
role: Literal["admin", "user", "viewer"]
@app.post("/users")
async def create_user(req: CreateUserRequest):
# req is already validated by Pydantic
...
```
### TypeScript (Zod + Express)
```typescript
import { z } from "zod";
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(200),
age: z.number().int().min(0).max(150),
role: z.enum(["admin", "user", "viewer"]),
});
app.post("/users", (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.issues });
}
// result.data is typed and validated
});
```
### Tools
| Language | Library | Purpose |
|---|---|---|
| Python | Pydantic, marshmallow, cerberus | Schema validation |
| TypeScript | Zod, Yup, io-ts, Ajv | Schema validation |
| Any | JSON Schema | Language-agnostic schema |
## Layer 2: Business Logic
**Purpose**: Enforce domain rules, state transitions, and authorization.
### What to Validate
- Business rules (e.g., "cannot cancel a shipped order")
- State machine transitions (e.g., draft -> published, not draft -> archived)
- Cross-field dependencies (e.g., "end_date must be after start_date")
- Authorization (e.g., "only the owner can modify this resource")
- Resource existence (e.g., "referenced entity must exist")
- Idempotency and duplicate detection
### Python
```python
class OrderService:
def cancel_order(self, order_id: str, user_id: str) -> Order:
order = self.repo.get(order_id)
if order is None:
raise NotFoundError(f"Order {order_id} not found")
if order.owner_id != user_id:
raise ForbiddenError("Only the order owner can cancel")
if order.status not in ("pending", "confirmed"):
raise BusinessRuleError(
f"Cannot cancel order in '{order.status}' status"
)
order.status = "cancelled"
return self.repo.save(order)
```
### TypeScript
```typescript
class OrderService {
cancelOrder(orderId: string, userId: string): Order {
const order = this.repo.get(orderId);
if (!order) throw new NotFoundError(`Order ${orderId} not found`);
if (order.ownerId !== userId) throw new ForbiddenError("Only the order owner can cancel");
const cancellableStatuses = ["pending", "confirmed"] as const;
if (!cancellableStatuses.includes(order.status)) {
throw new BusinessRuleError(`Cannot cancel order in '${order.status}' status`);
}
order.status = "cancelled";
return this.repo.save(order);
}
}
```
### Guidelines
- Keep validation logic in the service/domain layer, not in controllers
- Use custom exception types that map to HTTP status codes
- Business rules should be testable independently of HTTP/DB
## Layer 3: Data Persistence
**Purpose**: Enforce data integrity at the database level as the last line of defense.
### What to Validate
- NOT NULL constraints
- UNIQUE constraints (email, username)
- FOREIGN KEY constraints (referential integrity)
- CHECK constraints (value ranges, enums)
- Data types and precision
- Default values
### PostgreSQL Examples
```sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(200) NOT NULL CHECK (char_length(name) > 0),
age INTEGER CHECK (age >= 0 AND age <= 150),
role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'user', 'viewer')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
status VARCHAR(20) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'confirmed', 'shipped', 'cancelled')),
total_cents INTEGER NOT NULL CHECK (total_cents >= 0)
);
```
### Guidelines
- Mirror constraints in your ORM (SQLAlchemy `CheckConstraint`, Prisma `@unique`, etc.)
- Database constraints are the safety net; they catch bugs in application code
- Always handle constraint violation errors gracefully (unique violation -> 409 Conflict)
- Use migrations to manage schema changes
## Layer 4: Output Boundary
**Purpose**: Ensure responses are safe, well-formed, and contain only intended data.
### What to Validate
- Strip sensitive fields (passwords, internal IDs, tokens)
- HTML-encode user-generated content to prevent XSS
- Validate response schema (catch accidental data leaks)
- Set security headers (Content-Type, X-Content-Type-Options)
- Limit response size
### Techniques
- **Python**: Use Pydantic `response_model` to exclude fields not in the response schema
- **TypeScript**: Create explicit mapper functions (`toUserResponse()`) that pick only safe fields
- **Headers**: Set `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Content-Security-Policy`
- **Encoding**: HTML-encode user-generated content before rendering
## Layer Interaction Summary
| Layer | Catches | If Missing |
|---|---|---|
| Input | Malformed data, injection attempts | Bad data flows into business logic |
| Business | Invalid operations, auth bypass | Violated business rules, data corruption |
| Persistence | Constraint violations, duplicates | Inconsistent data in database |
| Output | Data leaks, XSS | Sensitive data exposed to clients |
@@ -1,9 +1,11 @@
---
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.
---
# Dispatching Parallel Agents
## Description
Pattern for handling multiple independent failures by dispatching concurrent agents. Use when 3+ independent problems exist across different domains that can be solved in parallel.
## When to Use
- Multiple subsystems broken independently
@@ -13,10 +15,9 @@ Pattern for handling multiple independent failures by dispatching concurrent age
## When NOT to Use
- Related failures (fixing one solves others)
- Exploratory debugging (need full context)
- Problems require shared understanding
- Sequential dependencies exist
- Tasks with shared state or sequential dependencies where one fix affects another
- Single-file changes that don't benefit from parallelization overhead
- Sequential workflows where each step depends on the output of the previous step
---
@@ -258,3 +259,8 @@ After parallel completion:
- [ ] Changes integrated successfully
---
## 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
@@ -0,0 +1,196 @@
# Parallelization Patterns Reference
How to decide what to parallelize and which pattern to use.
## Core Principle
Parallelize when tasks are **independent**: no shared mutable state, no ordering dependency, and results can be combined without conflict.
## Pattern 1: Independent Tasks
**When**: Two or more tasks share no state and have no ordering dependency.
**Always parallel.** This is the simplest and most common case.
### Examples
- Linting + type checking + unit tests (different tools, same codebase, read-only)
- Researching two unrelated libraries
- Generating tests for unrelated modules
- Reviewing separate files
### Structure
```
[Dispatcher]
|--- Agent A: lint src/
|--- Agent B: typecheck src/
|--- Agent C: run tests
\--- Agent D: security scan
[Collect all results]
```
### Decision Criteria
- Do they read/write the same files? No -> parallel
- Does one need output from another? No -> parallel
- Can they run in any order? Yes -> parallel
## Pattern 2: Fan-Out / Fan-In
**When**: A single task can be split into N identical subtasks, then results are merged.
### Examples
- Process each file in a directory independently
- Run the same analysis on multiple services
- Test multiple configurations
- Investigate multiple potential causes of a bug
### Structure
```
[Dispatcher: split work into N chunks]
|--- Agent 1: process chunk 1
|--- Agent 2: process chunk 2
|--- Agent 3: process chunk 3
\--- Agent N: process chunk N
[Collector: merge results from all agents]
```
### Implementation
Split items across agents (round-robin, by directory, or by type), dispatch all simultaneously, collect results, handle failures by retrying individually, then merge into unified output.
## Pattern 3: Pipeline (Sequential)
**When**: Output of step N is input to step N+1.
**Must be sequential.** Cannot parallelize.
### Examples
- Parse code -> analyze AST -> generate report
- Fetch data -> transform -> validate -> persist
- Write code -> run tests -> fix failures
### Structure
```
[Step 1: parse] --> [Step 2: analyze] --> [Step 3: report]
```
### When Pipelines Contain Parallelizable Steps
A pipeline stage itself might fan out:
```
[Step 1: identify files]
--> [Step 2: analyze each file in parallel (fan-out/fan-in)]
--> [Step 3: merge analysis into report]
```
## Pattern 4: Pipeline with Parallel Stages
**When**: Some pipeline stages can run in parallel, others must be sequential.
### Example: Feature Implementation
```
[Sequential: write plan]
--> [Parallel: implement module A, implement module B, implement module C]
--> [Sequential: integration test]
--> [Parallel: write docs, update changelog]
--> [Sequential: final review]
```
## Decision Matrix
| Task Characteristic | Pattern | Parallelizable? |
|---|---|---|
| No shared state, no ordering | Independent | Yes |
| Same operation on many items | Fan-out/fan-in | Yes |
| Output feeds next step | Pipeline | No |
| Mixed dependencies | Pipeline + parallel stages | Partially |
| Shared mutable state | Sequential or lock-based | No (usually) |
| Non-deterministic ordering matters | Sequential | No |
## Common Parallel Task Patterns
### File-Per-Agent
Split work by file or directory. Each agent owns its files exclusively.
```
Agent 1: src/auth/**
Agent 2: src/orders/**
Agent 3: src/users/**
```
**Best for**: code review, refactoring, test generation, documentation.
**Watch out for**: shared utilities, cross-module imports. Assign shared code to one agent or make it read-only for all.
### Test Suite Splitting
Split tests by module, type, or estimated runtime.
```
Agent 1: unit tests (fast)
Agent 2: integration tests (medium)
Agent 3: e2e tests (slow)
```
**Best for**: CI acceleration, pre-merge validation.
### Multi-Service Investigation
When debugging spans multiple services, assign one agent per service.
```
Agent 1: investigate auth service logs
Agent 2: investigate order service logs
Agent 3: investigate payment service logs
```
**Best for**: distributed system debugging, incident response.
### Research Branches
Explore multiple hypotheses or approaches simultaneously.
```
Agent 1: research approach A (Redis caching)
Agent 2: research approach B (CDN edge caching)
Agent 3: research approach C (application-level memoization)
```
**Best for**: technology evaluation, design exploration, root cause hypotheses.
## Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Parallelizing dependent tasks | Race conditions, wrong results | Identify dependencies first, use pipeline |
| Too many agents | Overhead exceeds benefit | 2-5 agents is typical sweet spot |
| No merge strategy | Results conflict or duplicate | Define merge/dedup logic before dispatching |
| Shared file writes | Corruption, lost changes | Assign file ownership to one agent |
| No failure handling | One failure blocks everything | Collect partial results, retry individually |
## Checklist Before Parallelizing
1. **List all tasks** that need to happen
2. **Draw dependencies** between them (which needs output from which?)
3. **Group independent tasks** into parallel batches
4. **Define the merge strategy** for collecting results
5. **Assign ownership** so no two agents write the same file
6. **Plan for failure** of individual agents
7. **Estimate whether parallelism helps** (overhead vs time saved)
## Quick Reference: Dispatch Decision
- Single atomic operation -> just do it, no parallelism
- Splittable into independent chunks -> fan-out/fan-in
- Each step depends on previous output -> pipeline (sequential)
- Mix of independent and dependent steps -> pipeline with parallel stages
- Everything independent -> run all in parallel
@@ -1,9 +1,11 @@
---
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.
---
# Executing Plans
## Description
Subagent-driven development pattern for executing detailed implementation plans with quality gates. Uses fresh agents per task and mandatory code review between tasks.
## When to Use
- Executing plans created with `writing-plans` skill
@@ -13,9 +15,9 @@ Subagent-driven development pattern for executing detailed implementation plans
## When NOT to Use
- Plan needs review first (use brainstorming)
- Tasks are tightly coupled and need shared context
- Plan requires revision during execution
- No plan exists yet -- use `writing-plans` first to create one
- Single-task work that does not need sequential execution or review gates
- Research or exploration where the goal is learning, not building
---
@@ -257,3 +259,9 @@ Before declaring plan execution complete:
- [ ] Ready for `finishing-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
@@ -0,0 +1,110 @@
# Plan Execution Checklist
Step-by-step checklist for executing implementation plans. Follow this sequence for each plan to ensure consistent, high-quality delivery.
---
## Phase 1: Pre-Execution
Complete all items before writing any code.
- [ ] **Read the full plan end-to-end** — Understand the complete scope before starting any task. Do not start task 1 without knowing what task N requires.
- [ ] **Identify the dependency graph** — Which tasks depend on others? Which can run in parallel? Mark the critical path.
- [ ] **Check external dependencies** — API keys available? Services running? Permissions granted? Third-party accounts set up?
- [ ] **Verify the environment**
- [ ] Correct branch checked out (or worktree created)
- [ ] Dependencies installed and up to date
- [ ] Existing tests pass before any changes
- [ ] Build succeeds from clean state
- [ ] **Clarify ambiguities** — If any task description is unclear, resolve it now. Do not guess during implementation.
- [ ] **Estimate total effort** — Does the sum of task estimates feel realistic given what you know? Flag concerns early.
---
## Phase 2: Per-Task Execution
Repeat for each task in plan order (respecting dependencies).
### Before Starting the Task
- [ ] **Read the task spec completely** — Including files to modify, changes, tests, and verification steps
- [ ] **Confirm dependencies are met** — All prerequisite tasks marked complete and verified
- [ ] **Check current state** — Run tests, confirm the codebase is in a good state before making changes
### During the Task
- [ ] **Write tests first** — If the plan includes tests for this task, write them before the implementation. They should fail initially.
- [ ] **Implement the changes** — Follow the spec. If you need to deviate, document why.
- [ ] **Run the task's specific tests** — All tests for this task must pass
- [ ] **Run the full test suite** — Ensure no regressions from your changes
- [ ] **Complete the task's verification steps** — Every verification item in the plan must be checked
### After Completing the Task
- [ ] **Mark the task complete** — Update the plan document
- [ ] **Check for side effects** — Did anything unexpected break? Are there warnings?
- [ ] **Commit the work** — One commit per task with a clear message referencing the plan
```
feat(scope): task description
Plan: [plan-name], Task N
```
- [ ] **Update the plan if needed** — If you discovered something that affects later tasks, note it now
---
## Phase 3: Post-Execution
Complete after all tasks are done.
### Verification
- [ ] **Run the full test suite** — All tests pass, not just the ones you added
```bash
# Python
pytest -v --cov=src
# TypeScript
pnpm test
```
- [ ] **Run the build** — Confirm the project builds without errors
```bash
pnpm build # or equivalent
```
- [ ] **Run linters and type checks** — No new warnings or errors
- [ ] **Manual verification** — Walk through the acceptance criteria in the plan's Verification Plan section
- [ ] **Check for leftover artifacts**
- [ ] No TODO comments left unresolved
- [ ] No commented-out code
- [ ] No debug logging left in place
- [ ] No temporary files committed
### Review
- [ ] **Self-review the diff** — Read your own changes as if reviewing someone else's PR
```bash
git diff main...HEAD
```
- [ ] **Check test quality** — Do tests verify behavior, not implementation? Are edge cases covered?
- [ ] **Check documentation** — If the plan required doc updates, are they done?
- [ ] **Verify acceptance criteria** — Every criterion in the plan marked as met
### Completion
- [ ] **Update plan status** — Mark as "Complete"
- [ ] **Summarize deviations** — Document any changes from the original plan and why
- [ ] **Create PR or merge** — Follow the project's git workflow
- [ ] **Clean up** — Remove worktree if used, close related issues
---
## Quick Reference: Common Failure Points
| Failure | Prevention |
|---------|-----------|
| Skipping plan review, then discovering blockers mid-task | Always complete Phase 1 fully |
| Tests pass in isolation but fail together | Run full suite after every task |
| Deviation from plan without updating it | Document changes as you make them |
| "It works on my machine" | Verify in clean environment |
| Forgetting to commit per-task | Commit immediately after verification |
| Side effects in later tasks | Check for regressions after each task |
@@ -1,9 +1,11 @@
---
name: finishing-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.
---
# Finishing a Development Branch
## Description
Structured 5-step workflow for completing development branches. Ensures tests pass, presents completion options, and handles cleanup.
## When to Use
- After implementing a feature
@@ -11,6 +13,12 @@ Structured 5-step workflow for completing development branches. Ensures tests pa
- Ready to merge or create PR
- Cleaning up after development
## When NOT to Use
- Work is still in progress and not all planned changes have been implemented
- Tests are failing and need to be fixed before the branch can be finalized
- Uncommitted changes remain that have not been staged or committed yet
---
## The 5-Step Workflow
@@ -273,3 +281,9 @@ Never:
- Leave orphaned worktrees
---
## 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
@@ -0,0 +1,197 @@
# Branch Completion Checklist
Checklist and reference for completing a development branch and integrating work.
## Pre-Merge Checklist
### Code Quality
- [ ] All tests pass on the branch (`pytest -v` / `pnpm test`)
- [ ] No linting errors (`ruff check` / `eslint .`)
- [ ] Type checking passes (`mypy` / `tsc --noEmit`)
- [ ] No TODO/FIXME without a ticket reference
- [ ] No debugging artifacts (print statements, console.log, commented-out code)
- [ ] No hardcoded secrets, API keys, or credentials
### Review
- [ ] Code review requested and approved
- [ ] All review comments addressed (fixed, deferred with ticket, or discussed)
- [ ] No unresolved conversations in the PR
### Testing
- [ ] Unit tests added for new behavior
- [ ] Integration tests added for new endpoints/services
- [ ] Edge cases covered (empty input, max size, unauthorized, concurrent)
- [ ] Test coverage meets minimum threshold (80% overall, 95% critical paths)
- [ ] Manual testing completed for UI/UX changes
### Documentation
- [ ] Public API documentation updated (docstrings, OpenAPI spec)
- [ ] README updated (if setup steps changed)
- [ ] CHANGELOG entry added (if applicable)
- [ ] Migration guide written (if breaking changes)
- [ ] Architecture/design docs updated (if structural changes)
### Branch Hygiene
- [ ] Branch is up to date with main (rebase or merge)
- [ ] No merge conflicts
- [ ] Commit history is clean and meaningful
- [ ] Branch name follows convention (`feature/`, `fix/`, `hotfix/`, `chore/`)
### CI/CD
- [ ] CI pipeline is green (all checks pass)
- [ ] Build succeeds
- [ ] No new warnings introduced
- [ ] Performance benchmarks pass (if applicable)
- [ ] Security scan passes (if applicable)
### Database/Infrastructure
- [ ] Migrations are reversible
- [ ] Migrations have been tested (up and down)
- [ ] No destructive schema changes without a migration plan
- [ ] Environment variables documented (if new ones added)
- [ ] Feature flags configured (if using progressive rollout)
## Merge Strategy Decision
### Merge Commit (`git merge --no-ff`)
**When to use:**
- Feature branch with multiple meaningful commits
- You want to preserve the full development history
- Team convention requires merge commits
**Result:** Preserves all commits plus a merge commit. Creates a clear merge point in history.
```bash
git checkout main
git merge --no-ff feature/TICKET-123-description
```
### Squash Merge (`git merge --squash`)
**When to use:**
- Feature branch has messy/WIP commits
- The feature is a single logical unit
- You want a clean linear history on main
**Result:** All commits become one commit on main.
```bash
git checkout main
git merge --squash feature/TICKET-123-description
git commit -m "feat(orders): add bulk order cancellation (#123)"
```
### Rebase (`git rebase main` + fast-forward merge)
**When to use:**
- Small number of clean, atomic commits
- You want linear history without merge commits
- Each commit builds on the previous logically
**Result:** Commits are replayed on top of main. No merge commit.
```bash
git checkout feature/TICKET-123-description
git rebase main
git checkout main
git merge --ff-only feature/TICKET-123-description
```
### Decision Matrix
| Situation | Strategy |
|---|---|
| Feature with messy WIP commits | Squash |
| Feature with clean, meaningful commits | Merge commit or rebase |
| Single commit fix | Fast-forward (rebase) |
| Long-lived branch, multiple contributors | Merge commit |
| Team prefers linear history | Squash or rebase |
| Need to bisect individual changes later | Merge commit or rebase (not squash) |
## Update Branch Before Merging
### Option A: Rebase onto main
```bash
git checkout feature/TICKET-123-description
git fetch origin
git rebase origin/main
# Resolve conflicts if any
git push --force-with-lease # update remote branch
```
**Pros:** Clean linear history.
**Cons:** Rewrites history (don't use if others are working on the branch).
### Option B: Merge main into branch
```bash
git checkout feature/TICKET-123-description
git fetch origin
git merge origin/main
# Resolve conflicts if any
git push
```
**Pros:** Safe, preserves history, works with shared branches.
**Cons:** Adds merge commits to the feature branch.
## Post-Merge Steps
### Immediate
- [ ] Delete the feature branch (local and remote)
```bash
git branch -d feature/TICKET-123-description
git push origin --delete feature/TICKET-123-description
```
- [ ] Verify main branch builds and tests pass
- [ ] Verify deployment to staging/preview environment succeeds
### Follow-Up
- [ ] Close the associated ticket/issue
- [ ] Notify the team (if significant change)
- [ ] Monitor logs and error rates after deployment
- [ ] Verify the feature works in the deployed environment
- [ ] Update project board/tracker
### If Something Goes Wrong
| Problem | Action |
|---|---|
| Tests fail on main after merge | Revert the merge commit immediately, investigate on a new branch |
| Deployment fails | Roll back deployment, investigate, do not push fixes to main under pressure |
| Bug found in production | Create a hotfix branch from main, fix, test, deploy |
| Need to undo a squash merge | `git revert <squash-commit-sha>` |
| Need to undo a merge commit | `git revert -m 1 <merge-commit-sha>` |
## Quick Reference: Common Commands
```bash
# Check if branch is up to date with main
git fetch origin && git log HEAD..origin/main --oneline
# See what will be merged
git log main..HEAD --oneline
# See the full diff against main
git diff main...HEAD
# Check CI status (GitHub CLI)
gh pr checks
# Merge via GitHub CLI
gh pr merge --squash # or --merge, --rebase
# Delete branch after merge
gh pr merge --squash --delete-branch
```
@@ -1,9 +1,11 @@
---
name: receiving-code-review
description: >
Trigger this skill whenever code review feedback is received, whether from human reviewers, automated tools, or PR comments. Use when processing review comments, handling review rejections, iterating on feedback cycles, or deciding how to prioritize critical vs minor issues. Activate aggressively any time review feedback arrives -- categorize, prioritize, fix critical issues first, and re-request review with a clear summary of changes made.
---
# Receiving Code Review
## Description
Workflow for processing code review feedback effectively. Prioritize issues, apply fixes, and iterate until approval.
## When to Use
- After receiving review feedback
@@ -11,6 +13,12 @@ Workflow for processing code review feedback effectively. Prioritize issues, app
- Handling reviewer comments on PRs
- Iterating after code review rejection
## When NOT to Use
- Self-review of your own code where an independent perspective is what you actually need
- Initial implementation before any review has been requested or received
- Design or brainstorming phase where feedback is about ideas, not code
---
## Feedback Categories
@@ -256,3 +264,9 @@ If review requires 3+ cycles:
```
---
## Related Skills
- `methodology/requesting-code-review` - Companion skill for initiating reviews with proper context before feedback is received
- `methodology/systematic-debugging` - Use systematic debugging techniques when review feedback reveals bugs that need investigation
- `methodology/verification-before-completion` - After addressing review feedback, verify all fixes before claiming completion
@@ -0,0 +1,190 @@
# Feedback Categories Reference
How to categorize, prioritize, and respond to code review feedback.
## Category Definitions
### Critical -- Must Fix Before Merge
**Impact**: Security vulnerability, data loss, crash, or correctness failure.
**Examples**:
- SQL injection or XSS vulnerability
- Missing authentication/authorization check
- Data corruption or silent data loss
- Unhandled exception that crashes the service
- Race condition that causes incorrect results
- Breaking change to public API without migration path
**Response**: Fix immediately. No merge until resolved. Thank the reviewer.
**Time**: Address within hours, not days.
### Important -- Should Fix
**Impact**: Logic error, missing edge case, performance issue, or maintainability concern.
**Examples**:
- Missing null/undefined check on a code path that can be reached
- N+1 query that will degrade with data growth
- Missing error handling for a plausible failure mode
- Incorrect business logic for an edge case
- Missing test for a significant code path
- Resource leak (connection, file handle, memory)
**Response**: Fix before merge unless there is a strong reason to defer (document with a ticket if deferring).
**Time**: Address before the next review round.
### Minor -- Fix If Easy
**Impact**: Code style, naming, comments, minor readability.
**Examples**:
- Variable name could be clearer
- Comment is slightly inaccurate
- Could extract a helper function for readability
- Import ordering
- Unnecessary intermediate variable
- Slightly verbose code that could be simplified
**Response**: Fix if the change is quick and low-risk. If fixing would require significant refactoring, note it for a follow-up.
**Time**: Address in the current PR or create a follow-up ticket.
### Subjective -- Discuss and Decide
**Impact**: Architectural preference, design philosophy, style choice where both options are valid.
**Examples**:
- "I would have used a class here instead of functions"
- "I prefer early returns over nested if-else"
- "Consider using pattern X instead of pattern Y"
- "This could also be modeled as an event-driven system"
- Disagreement on level of abstraction
**Response**: Engage in discussion. Consider the merits. Agree on a direction or escalate to team lead. Neither side is necessarily wrong.
**Time**: Resolve within one discussion round if possible.
## Prioritization Matrix
| Category | Merge Blocker? | Default Action | Can Defer? |
|---|---|---|---|
| Critical | Yes | Fix now | No |
| Important | Usually | Fix now or create ticket | With justification |
| Minor | No | Fix if quick | Yes, with follow-up |
| Subjective | No | Discuss | Yes, team decision |
## How to Handle Each Category
### Receiving Critical Feedback
1. Acknowledge the issue immediately
2. Do not be defensive -- this is protecting users
3. Fix and push the update
4. Add a test that would catch the issue
5. Consider if similar issues exist elsewhere
```
> Reviewer: This SQL query uses string interpolation, which is vulnerable to injection.
>
> You: Good catch -- fixed in abc1234. Added parameterized query and a test
> that verifies injection attempts are escaped. Also checked the other
> queries in this module; they all use parameterized queries already.
```
### Receiving Important Feedback
1. Evaluate whether the feedback is correct (verify, don't assume)
2. If correct, fix it
3. If you disagree, explain your reasoning with evidence
4. If deferring, create a ticket and reference it
```
> Reviewer: This will N+1 query when loading orders with items.
>
> You: You're right. Added eager loading with joinedload() in commit def5678.
> Added a test that asserts query count stays constant regardless of item count.
```
### Receiving Minor Feedback
1. Fix quickly if possible
2. If it requires significant refactoring, note it
```
> Reviewer: Consider renaming `data` to `order_summary` for clarity.
>
> You: Renamed in abc9012. Agreed it's clearer.
```
or
```
> Reviewer: This function could be extracted into a utility.
>
> You: Agree, but it's only used here for now. Created PROJ-789 to extract
> it if we need it elsewhere. Keeping it inline for this PR.
```
### Receiving Subjective Feedback
1. Consider the suggestion genuinely
2. Present your reasoning if you disagree
3. Look for objective criteria to decide (performance, testability, consistency with codebase)
4. If no clear winner, defer to existing codebase conventions
5. If still no consensus, the code author decides (or escalate)
```
> Reviewer: I'd prefer a class-based approach here.
>
> You: I considered that. Went with functions because: (1) no shared state
> between operations, (2) matches the pattern in src/services/auth.py,
> (3) easier to test in isolation. Happy to discuss further if you see
> benefits I'm missing.
```
## Handling Disagreements
### Step-by-Step Process
1. **Verify the claim**: Run the test, check the docs, reproduce the scenario. Do not argue from assumption.
2. **Propose an alternative**: If you disagree, suggest what you would do instead and explain why.
3. **Look for objective evidence**: Benchmarks, test results, documentation, or existing patterns in the codebase.
4. **Find common ground**: Often both approaches have merit. Look for a synthesis.
5. **Escalate if stuck**: Bring in a third opinion (tech lead, team discussion). Do not let PRs stall.
### What NOT to Do
- Do not dismiss feedback without investigation
- Do not agree with everything to avoid conflict (performative agreement hides bugs)
- Do not take feedback personally
- Do not let disagreements block merges for days -- timebox the discussion
- Do not relitigate decisions that were already agreed upon by the team
## Feedback Response Checklist
For each piece of feedback received:
- [ ] Read and understand the feedback fully
- [ ] Categorize it (critical / important / minor / subjective)
- [ ] If technical claim: verify it independently (run the code, check docs)
- [ ] Respond with what you did (fixed, deferred with ticket, or discussed)
- [ ] If fixed: reference the commit
- [ ] If deferred: reference the ticket
- [ ] If disagreeing: provide reasoning with evidence
## Quick Reference: Response Templates
**Agreeing and fixing:**
> Fixed in [commit]. Added test to prevent regression.
**Agreeing and deferring:**
> Agreed. Created [TICKET] to address this. Out of scope for this PR.
**Disagreeing with reasoning:**
> Considered this. Went with [approach] because [reason 1], [reason 2]. Here's [evidence]. Open to discussion.
**Asking for clarification:**
> Can you clarify what you mean by [X]? I want to make sure I address the right concern.
@@ -1,9 +1,11 @@
---
name: requesting-code-review
description: >
Trigger this skill after completing any task, implementing a feature, fixing a critical bug, or before merging to a main branch. Use whenever code is ready for feedback, when unsure about an implementation approach, or when changes touch security, authentication, or data handling. Activate before any PR creation or branch merge to ensure reviewers have complete context, clear scope, and focused areas of concern.
---
# Requesting Code Review
## Description
Workflow for initiating code reviews with clear scope, context, and expectations. Ensures reviewers have everything needed for effective feedback.
## When to Use
- After completing a task (before proceeding to next)
@@ -12,6 +14,12 @@ Workflow for initiating code reviews with clear scope, context, and expectations
- When unsure about implementation approach
- After fixing critical bugs
## When NOT to Use
- Mid-implementation work where the code is still incomplete and likely to change significantly
- Research or exploration tasks where you are prototyping and not producing production code
- Trivial one-line fixes like typo corrections or version bumps that carry no risk
---
## Review Request Components
@@ -237,3 +245,9 @@ Reviewer will return:
See `receiving-code-review` skill for detailed guidance.
---
## Related Skills
- `methodology/receiving-code-review` - Companion skill for processing and acting on review feedback after it is received
- `methodology/verification-before-completion` - Run verification checks before requesting review to ensure code is actually ready
- `methodology/finishing-development-branch` - Use after review approval to complete the branch merge/PR workflow
@@ -0,0 +1,143 @@
# Review Request Template
Use this template when requesting code review. Copy the structure below and fill in each section. Remove sections that are not applicable, but err on the side of including more context.
---
## Review Request
### Summary
_One to three sentences describing the change at a high level. What does this change do and why?_
**Type**: `feature` | `bugfix` | `refactor` | `performance` | `security` | `chore`
**Ticket/Issue**: [Link or ID]
**Branch**: `feature/TICKET-123-description` -> `main`
---
### Changes Made
_List the key changes. Group by area if touching multiple parts of the codebase._
**Core changes:**
- [ ] Changed X in `src/path/to/file.py` to support Y
- [ ] Added new endpoint `POST /api/resource` in `src/api/routes.py`
- [ ] Updated database schema: added `column_name` to `table_name`
**Supporting changes:**
- [ ] Added migration `migrations/0042_add_column.py`
- [ ] Updated config for new feature flag `ENABLE_FEATURE_X`
**Files changed:** _N files, +X/-Y lines_ (or let the PR tool calculate)
---
### Testing Done
_Describe what testing was performed. Be specific._
- [ ] Unit tests added/updated: `tests/test_feature.py`
- [ ] Integration tests added/updated: `tests/integration/test_api.py`
- [ ] Manual testing steps:
1. Step one
2. Step two
3. Expected result
- [ ] Edge cases tested:
- Empty input
- Maximum size input
- Unauthorized user
- Concurrent requests
- [ ] All existing tests pass: `pytest -v` / `pnpm test`
---
### Areas of Concern
_Be honest about parts you are unsure about. This helps reviewers focus._
- [ ] The caching logic in `src/services/cache.py` lines 42-67 may have race conditions under high concurrency
- [ ] Not sure if the error handling in `handleTimeout()` covers all edge cases
- [ ] Performance impact of the new query has not been benchmarked
- [ ] _None -- I am confident in this change_
---
### Reviewer Focus Areas
_Tell the reviewer where to spend their time. Rank by priority._
1. **Security**: Authentication logic in `src/auth/middleware.py` -- does the token validation cover all cases?
2. **Correctness**: State machine transitions in `src/services/order.py` -- are all transitions valid?
3. **Performance**: New database query in `src/repos/order_repo.py` -- is it using the right index?
4. **Design**: Is the service layer abstraction appropriate, or should this be split?
---
### How to Test Locally
_Step-by-step instructions so the reviewer can verify the change._
```bash
# 1. Set up environment
git checkout feature/TICKET-123-description
pip install -r requirements.txt # or: pnpm install
# 2. Run migrations (if applicable)
python manage.py migrate # or: pnpm db:migrate
# 3. Set required environment variables (if applicable)
export FEATURE_X_ENABLED=true
# 4. Run the application
python -m uvicorn main:app --reload # or: pnpm dev
# 5. Test the change
curl -X POST http://localhost:8000/api/resource \
-H "Content-Type: application/json" \
-d '{"key": "value"}'
# Expected: 201 Created with response body { "id": "...", "key": "value" }
# 6. Run tests
pytest tests/ -v # or: pnpm test
```
---
### Additional Context
_Optional. Screenshots, diagrams, links to design docs, related PRs, or anything else that helps the reviewer._
- Design doc: [link]
- Related PR: #42
- Screenshot of UI change: [attached]
- Before/after performance metrics: [data]
---
## Quick Version (For Small Changes)
For small, low-risk changes, use this abbreviated format:
```
## Review Request
**Summary**: Fix off-by-one in pagination (returns N+1 results instead of N)
**Ticket**: PROJ-456
**Changes**: `src/api/pagination.py` line 23: `< limit` changed to `<= limit`
**Tests**: Updated `tests/test_pagination.py`, all pass
**Risk**: Low -- single line change, well-covered by tests
```
---
## Checklist Before Submitting
- [ ] Self-reviewed the diff (read your own PR as if you were the reviewer)
- [ ] Tests added for new behavior
- [ ] No TODO/FIXME/HACK comments left without a ticket reference
- [ ] No debugging artifacts (print statements, console.log, commented-out code)
- [ ] Documentation updated (if user-facing behavior changed)
- [ ] Migration is reversible (if schema changed)
- [ ] No secrets in the diff
@@ -1,9 +1,11 @@
---
name: root-cause-tracing
description: >
Trigger this skill whenever a bug manifests far from its origin, when stack traces show multiple layers of indirection, or when data corruption appears with no obvious source. Use for any scenario involving "it was already wrong by the time it got here," deep execution stack errors, constraint violations caused by upstream failures, or mysterious data state issues. Always prefer this over surface-level fixes when the error location differs from the bug location.
---
# Root Cause Tracing
## Description
Debugging technique for bugs that manifest deep in execution stacks. Systematically trace backward through the call chain to identify the original trigger, rather than fixing symptoms at the point of failure.
## When to Use
- Errors occur far from entry points
@@ -12,6 +14,12 @@ Debugging technique for bugs that manifest deep in execution stacks. Systematica
- Stack traces show multiple levels of indirection
- "It was already wrong by the time it got here"
## When NOT to Use
- Surface-level UI bugs where the cause and effect are co-located
- Known issues with documented fixes already available in the codebase or issue tracker
- Performance optimization work where profiling tools are more appropriate than tracing
---
## Core Principle
@@ -228,3 +236,9 @@ Fixing at the source:
- [ ] Test proves fix works
---
## Related Skills
- `methodology/systematic-debugging` - General debugging methodology; use root-cause-tracing when the bug location differs from the error location
- `methodology/defense-in-depth` - After tracing the root cause, apply multi-layer validation to make the bug structurally impossible
- `methodology/sequential-thinking` - Use sequential thinking to systematically document evidence and hypotheses during complex tracing sessions
@@ -0,0 +1,168 @@
# Tracing Techniques Reference
Backward-tracing techniques for systematic root cause analysis.
## Stack Trace Analysis
### Reading a Stack Trace
1. Start at the **bottom** (most recent call) to find the immediate failure
2. Scan **upward** to find the first frame in **your code** (not library code)
3. That frame is usually the symptom location, not the cause
4. Continue upward to find where bad data or state originated
### Symptom vs Cause
| What You See | Likely Actual Cause |
|---|---|
| `NullPointerException` / `TypeError: cannot read property of undefined` | Value not set upstream, missing null check at origin |
| `IndexOutOfBoundsException` | Off-by-one in loop logic or empty collection not guarded |
| `ConnectionRefusedError` | Service down, wrong port, firewall rule, DNS resolution |
| `TimeoutError` | Deadlock, resource exhaustion, slow query, network partition |
| `ValidationError` | Caller passing wrong shape, schema mismatch, migration gap |
### Tips
- Filter out framework frames to reduce noise
- In async code, the stack may be split; look for `caused by` or `previous` sections
- In Python, read `__cause__` and `__context__` on chained exceptions
- In TypeScript/Node, check `error.cause` (ES2022+)
## Binary Search / Git Bisect
### When to Use
- Bug exists now but worked at some known-good point
- Reproducer is automatable (script, test command)
### Process
```bash
git bisect start
git bisect bad # current commit is broken
git bisect good <known-good-sha> # last known working commit
# Git checks out a midpoint; run your test
git bisect good # or bad, based on result
# Repeat until Git identifies the first bad commit
git bisect reset # return to original branch
```
### Automated Bisect
```bash
git bisect start HEAD <good-sha>
git bisect run ./test-script.sh
# Exit 0 = good, exit 1 = bad, exit 125 = skip
```
## Log Correlation
### Technique
1. Identify the **exact timestamp** of the error
2. Search all related service logs within a window (e.g., +/- 30 seconds)
3. Filter by **correlation ID**, **request ID**, or **user ID** across services
4. Build a timeline of events across services
### Correlation Fields to Look For
- `request_id` or `trace_id` (distributed tracing)
- `user_id` or `session_id`
- Source IP or client identifier
- Timestamps (normalize to UTC)
### Tools
- `grep` / `rg` with timestamp ranges
- Structured logging with JSON output + `jq`
- Distributed tracing (OpenTelemetry, Jaeger, Zipkin)
## Dependency Analysis (Backward Data Flow)
### Process
1. Start at the error location
2. Identify the **variable or value** that is wrong
3. Trace backward: where was this value set?
4. At each step, ask: is this value correct here? If yes, move forward. If no, keep going back.
5. The root cause is where correct data first becomes incorrect.
### Common Data Flow Points
```
User Input -> Validation -> Transform -> Business Logic -> Persistence -> Query -> Response
```
Trace backward through this chain from wherever the error manifests.
### Dependency Categories
| Dependency | What to Check |
|---|---|
| Function arguments | Caller passing wrong values |
| Config / env vars | Wrong environment, stale config |
| Database state | Missing migration, corrupt data |
| External API | Changed response format, auth expiry |
| Shared state | Race condition, stale cache |
## Instrumentation Points
### Where to Add Temporary Logging
1. **Entry/exit of suspected function** — log arguments and return value
2. **Before/after external calls** — log request and response
3. **Branch points** — log which path was taken and why
4. **Data transformation steps** — log before and after
5. **Error handlers** — log the full error with context
### Guidelines
- Use a distinct prefix (e.g., `[DEBUG-TRACE]`) so logs are easy to find and remove
- Log the **type** as well as the **value** (catches `"null"` vs `null`)
- In production, use feature flags or debug log levels, not code changes
- Remove all temporary logging before committing
### Python Example
```python
import logging
logger = logging.getLogger(__name__)
def process_order(order_id: str) -> Order:
logger.debug("[DEBUG-TRACE] process_order called with: %s (type: %s)", order_id, type(order_id))
order = db.get_order(order_id)
logger.debug("[DEBUG-TRACE] db.get_order returned: %s", order)
# ... rest of logic
```
### TypeScript Example
```typescript
function processOrder(orderId: string): Order {
console.debug(`[DEBUG-TRACE] processOrder called with: ${orderId} (type: ${typeof orderId})`);
const order = db.getOrder(orderId);
console.debug(`[DEBUG-TRACE] db.getOrder returned:`, order);
// ... rest of logic
}
```
## Common Root Cause Categories
| Category | Symptoms | Investigation Approach |
|---|---|---|
| **Data issues** | Wrong output, validation errors, corrupt state | Trace the bad value backward through the data flow |
| **Race conditions** | Intermittent failures, works-on-retry, order-dependent | Look for shared mutable state, add timing logs, test with delays |
| **Config drift** | Works locally but not in staging/prod | Diff environment configs, check env vars, verify secrets |
| **Dependency changes** | Broke after deploy with no code changes | Check lock file diffs, dependency changelogs, API version headers |
| **Resource exhaustion** | Timeouts, OOM, connection pool errors | Monitor metrics (memory, CPU, connections, disk), check for leaks |
| **Schema mismatch** | Serialization errors, missing fields | Compare expected vs actual schema, check migration status |
## Quick Decision: Which Technique to Use
| Situation | Start With |
|---|---|
| Have a stack trace | Stack trace analysis |
| "It used to work" | Git bisect |
| Multi-service issue | Log correlation |
| Wrong data in output | Backward data flow |
| No idea where to start | Add instrumentation at boundaries |
@@ -1,9 +1,11 @@
---
name: sequential-thinking
description: >
Trigger this skill for any complex problem requiring careful step-by-step reasoning, evidence collection, and confidence tracking. Use when debugging has multiple possible causes, when making architecture decisions with trade-offs, during security analysis or audits, for performance investigations, or whenever decisions need explicit documentation. Activate aggressively for any scenario where jumping to conclusions would be risky or where the reasoning chain matters as much as the answer.
---
# Sequential Thinking
## Description
Step-by-step reasoning methodology with explicit evidence collection and confidence tracking. Use for complex problems requiring careful analysis and documented decision-making.
## When to Use
- Complex debugging
@@ -13,6 +15,12 @@ Step-by-step reasoning methodology with explicit evidence collection and confide
- Any problem with multiple possible causes
- When decisions need documentation
## When NOT to Use
- Simple straightforward tasks where the answer is obvious and well-known
- Mechanical code changes like renames, formatting, or boilerplate generation
- When the MCP sequential-thinking server is unavailable and structured tool support is needed
---
## The Sequential Process
@@ -243,3 +251,9 @@ needsMoreThoughts: If more analysis needed
- Performance investigations
---
## Related Skills
- `methodology/brainstorming` - Use brainstorming for open-ended creative exploration; use sequential thinking when you need structured evidence-based analysis
- `methodology/root-cause-tracing` - Complements sequential thinking by providing the tracing methodology to follow during evidence gathering steps
- `methodology/systematic-debugging` - Use systematic debugging for the overall debugging framework; sequential thinking adds rigorous documentation and confidence tracking
@@ -1,9 +1,11 @@
---
name: systematic-debugging
description: >
Trigger this skill whenever encountering ANY bug, error, test failure, or unexpected behavior. Activate for keywords like "bug", "error", "failing", "broken", "doesn't work", "unexpected", "crash", "exception", "TypeError", "undefined", stack traces, or any error message. Also trigger when tests fail unexpectedly, when behavior differs from expectations, when investigating production incidents, or when flaky/intermittent issues appear. ALWAYS investigate root cause before proposing fixes -- never guess at solutions.
---
# Systematic Debugging
## Description
Four-phase debugging methodology centered on finding root causes before implementing fixes. The foundational principle: **"NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST"**
## When to Use
- Bug reports with unclear cause
@@ -12,6 +14,12 @@ Four-phase debugging methodology centered on finding root causes before implemen
- Intermittent/flaky issues
- Complex multi-component failures
## When NOT to Use
- Known issues with documented fixes already available in the codebase or runbook
- Simple typo or syntax errors that are immediately obvious from the error message
- Configuration issues where the fix is simply updating an environment variable or config value
---
## The Four Phases
@@ -256,3 +264,9 @@ Before declaring fixed:
- [ ] Fix explained (can articulate why it works)
---
## Related Skills
- `methodology/root-cause-tracing` -- Deep-dive technique for tracing issues back through complex dependency chains
- `methodology/defense-in-depth` -- Add defensive layers to prevent similar bugs from recurring
- `methodology/verification-before-completion` -- Ensures the fix is actually verified with evidence before claiming the bug is resolved
@@ -0,0 +1,155 @@
# Systematic Debugging Checklist
Step-by-step process for debugging any issue. Follow the steps in order — skipping ahead is the most common cause of wasted debugging time.
---
## Step 1: Reproduce
**Goal:** Confirm you can trigger the bug on demand.
- [ ] **Get the exact steps** — What did the user do? What input? What sequence?
- [ ] **Reproduce it yourself** — If you can't reproduce it, you can't verify a fix
- [ ] **Find the minimal reproduction** — Strip away everything that isn't necessary to trigger the bug
- [ ] **Document the environment**
- OS and version
- Language/runtime version
- Dependency versions
- Environment variables or config that matters
- [ ] **Note what you expect vs. what actually happens**
**If you can't reproduce:**
- Check if it's environment-specific (OS, browser, node version)
- Check if it's state-dependent (specific data, race condition, cache)
- Add logging and wait for it to happen again
---
## Step 2: Gather Evidence
**Goal:** Collect all available information before forming theories.
- [ ] **Read the error message carefully** — The answer is often in the message. Read the full text, not just the first line.
- [ ] **Read the full stack trace** — Identify which line in YOUR code is the entry point (ignore framework internals at first)
- [ ] **Check logs** — Application logs, server logs, browser console
- [ ] **Check timestamps** — When did it start? Does it correlate with a deployment, config change, or data change?
- [ ] **Check recent changes**
```bash
git log --oneline -20
git diff HEAD~5..HEAD -- path/to/suspect/area/
```
- [ ] **Check monitoring/metrics** — Error rates, latency, resource usage
- [ ] **Search for the error** — Has someone on the team seen this before? Check issues, Slack, docs.
---
## Step 3: Form Hypotheses
**Goal:** Generate candidate explanations ranked by likelihood.
- [ ] **What changed recently?** — The most common cause of new bugs is new code
- [ ] **What assumptions might be wrong?** — About input format, data state, timing, permissions
- [ ] **List 2-3 hypotheses** — Write them down explicitly:
1. [Most likely] ...
2. [Possible] ...
3. [Less likely but worth checking] ...
- [ ] **For each hypothesis, define what evidence would confirm or refute it**
**Common root causes to consider:**
- Null/undefined where a value was expected
- Off-by-one or boundary condition
- Race condition or timing issue
- Stale cache or state
- Environment difference (local vs. prod)
- Dependency version mismatch
- Incorrect assumption about API contract
---
## Step 4: Test Hypotheses
**Goal:** Confirm or eliminate each hypothesis with evidence.
- [ ] **Start with the most likely hypothesis**
- [ ] **Add targeted logging** — Log the specific values your hypothesis predicts will be wrong
```python
# Python
import logging
logger = logging.getLogger(__name__)
logger.debug(f"Value at suspect point: {value!r}, type: {type(value)}")
```
```javascript
// JavaScript
console.log('Value at suspect point:', JSON.stringify(value), typeof value);
```
- [ ] **Use git bisect for regressions** — Find the exact commit that introduced the bug
```bash
git bisect start
git bisect bad # Current commit is broken
git bisect good v1.2.0 # This version was working
# Test each commit bisect offers, mark good/bad
```
- [ ] **Isolate components** — Test each component in isolation to narrow the scope
- [ ] **Use a debugger for complex state issues**
### Debugger Quick Reference
| Language | Tool | Start Command |
|----------|------|--------------|
| Python | pdb | `import pdb; pdb.set_trace()` or `breakpoint()` |
| Python | logging | `logging.basicConfig(level=logging.DEBUG)` |
| Python | traceback | `import traceback; traceback.print_exc()` |
| JavaScript | debugger | `debugger;` statement in code |
| JavaScript | console | `console.log()`, `console.trace()`, `console.table()` |
| JavaScript | Chrome DevTools | Open DevTools > Sources > set breakpoint |
| TypeScript | Node inspect | `node --inspect -r ts-node/register script.ts` |
---
## Step 5: Fix and Verify
**Goal:** Apply the minimal correct fix and prove it works.
- [ ] **Make the smallest fix possible** — Fix the bug, not the whole file. One concern per commit.
- [ ] **Write a regression test** — A test that fails without your fix and passes with it
```python
def test_handles_empty_input_without_crash():
"""Regression test for bug #123 — empty input caused TypeError."""
result = process(input_data="")
assert result == expected_default
```
- [ ] **Verify the fix resolves the original reproduction**
- [ ] **Run the full test suite** — Confirm no side effects
- [ ] **Check related code paths** — Is the same bug pattern present elsewhere?
```bash
# Search for similar patterns
grep -rn "similar_pattern" src/
```
- [ ] **Test edge cases around the fix** — Boundary values, null inputs, concurrent access
---
## Step 6: Document and Prevent
**Goal:** Prevent this class of bug from recurring.
- [ ] **Write a clear commit message** explaining what was wrong and why the fix works
- [ ] **Update documentation** if the bug revealed a misunderstanding
- [ ] **Consider systemic fixes:**
- Could a type system catch this? (Add types)
- Could a linter rule catch this? (Add rule)
- Could input validation catch this? (Add validation)
- Could monitoring catch this sooner? (Add alert)
---
## Quick Reference: Debugging Anti-Patterns
| Anti-Pattern | What to Do Instead |
|-------------|-------------------|
| Changing random things until it works | Form a hypothesis, test it, iterate |
| Debugging in production | Reproduce locally first |
| Reading code for hours without running it | Add a log statement and run it |
| Fixing the symptom, not the cause | Ask "why?" until you reach the root |
| Not writing a regression test | Always write one before closing the bug |
| Debugging alone for too long | Ask for help after 30 minutes of no progress |
@@ -1,9 +1,11 @@
---
name: test-driven-development
description: >
Trigger this skill whenever writing new features, fixing bugs, or changing any behavior in production code. Activate for keywords like "implement", "add feature", "fix bug", "write code", "build", "create endpoint", "add functionality", or any task that will result in production code changes. Also trigger when the user asks to refactor existing code, when tests need to be written, or when someone says "TDD". This skill should be the default for ALL implementation work -- no production code without a failing test first.
---
# Test-Driven Development (TDD)
## Description
Strict test-driven development methodology requiring tests before implementation. The fundamental practice: "If you didn't watch the test fail, you don't know if it tests the right thing."
## When to Use
- New feature development
@@ -11,11 +13,11 @@ Strict test-driven development methodology requiring tests before implementation
- Refactoring (ensure tests exist before changing)
- Any behavior change
## When NOT to Use (Requires Explicit Approval)
## When NOT to Use
- Throwaway prototypes
- Generated/scaffolded code
- Pure configuration changes
- Prototyping or throwaway code with explicit user approval to skip tests
- Configuration-only changes (e.g., environment variables, CI config, linter rules)
- Documentation updates that do not affect runtime behavior
---
@@ -241,3 +243,11 @@ This is faster than:
7. Ship again
---
## Related Skills
- `methodology/verification-before-completion` -- Ensures tests are actually run and passing before claiming work is done
- `methodology/testing-anti-patterns` -- Avoid common testing mistakes that undermine TDD effectiveness
- `testing/pytest` -- Python-specific testing patterns and best practices for TDD
- `testing/vitest` -- TypeScript/JavaScript-specific testing patterns and best practices for TDD
- `methodology/writing-plans` — Planning implementation tasks for TDD workflow
@@ -0,0 +1,150 @@
# TDD Decision Tree
Quick reference for deciding when and how to apply Test-Driven Development.
---
## Decision: Should I Use TDD Here?
```
Is this code...
├─ Business logic or data transformation?
│ └─ YES: Always TDD. No exceptions.
├─ An API endpoint (REST, GraphQL, RPC)?
│ └─ YES: Always TDD. Write request/response tests first.
├─ A bug fix?
│ └─ YES: Always TDD. Write a failing test that reproduces the bug first.
├─ A utility function or helper?
│ └─ YES: Always TDD. These are the easiest to TDD — pure input/output.
├─ A database query or repository method?
│ └─ YES: Always TDD. Test the query behavior, not the SQL syntax.
├─ A state machine or workflow?
│ └─ YES: Always TDD. Test each transition.
├─ UI layout or styling (CSS, Tailwind, visual positioning)?
│ └─ TDD optional. Visual output is hard to assert meaningfully.
│ Use snapshot tests or visual regression tools instead.
├─ Configuration or environment setup?
│ └─ TDD optional. Test that config loads correctly, but don't
│ TDD every config value. Integration tests are more useful.
├─ A database migration?
│ └─ TDD optional. Test that migration runs forward and backward.
│ Don't TDD the migration SQL itself.
├─ A prototype or spike?
│ └─ TDD optional. Spikes are throwaway. But if the spike becomes
│ real code, stop and add tests before continuing.
├─ Third-party integration glue code?
│ └─ TDD the contract, not the integration. Write tests against
│ the interface you expect, mock the external service.
└─ Generated code (scaffolding, boilerplate)?
└─ TDD optional. Test the generator if you wrote it.
Don't TDD the generated output.
```
---
## Decision Factors
When the tree above doesn't give a clear answer, weigh these factors:
| Factor | Favors TDD | Favors Test-After |
|--------|-----------|-------------------|
| **Testability** | Clear inputs/outputs, deterministic | Heavy side effects, UI rendering |
| **Complexity** | Multiple branches, edge cases | Straightforward single-path logic |
| **Risk** | Failure is costly (data loss, security) | Failure is cosmetic or low-impact |
| **Stability** | Requirements are clear and stable | Requirements are still changing |
| **Team convention** | Team expects TDD | Team doesn't practice TDD |
| **Confidence** | You're unsure how to implement it | You've built this exact thing before |
**Rule of thumb:** If you're unsure, use TDD. The cost of writing a test first is low. The cost of a bug in untested code is high.
---
## The TDD Cycle
```
1. RED — Write a failing test that defines the desired behavior
2. GREEN — Write the minimum code to make the test pass
3. REFACTOR — Clean up without changing behavior (tests still pass)
4. REPEAT — Next behavior
```
### Common Mistakes
- **Writing too much test at once** — Test one behavior per cycle
- **Writing implementation before the test fails** — The failing test is the spec
- **Skipping refactor** — Technical debt accumulates in GREEN if you don't clean up
- **Testing implementation details** — Test what it does, not how it does it
---
## Handling Legacy Code Without Tests
Legacy code (code without tests) requires a different entry point into TDD.
### Step 1: Characterization Tests
Before changing anything, write tests that capture current behavior:
```python
# Characterization test — documents what the code DOES, not what it SHOULD do
def test_calculate_total_current_behavior():
result = calculate_total(items=[{"price": 10, "qty": 2}])
assert result == 20 # Observed behavior, may or may not be correct
```
### Step 2: Identify the Change Boundary
What's the smallest piece of code you need to change? Draw a boundary around it.
### Step 3: Add Seams
If the code is untestable (hard dependencies, global state), add seams:
- Extract method
- Inject dependencies
- Wrap external calls
### Step 4: TDD the Change
Now that you have characterization tests protecting existing behavior and seams allowing isolation, use the normal RED-GREEN-REFACTOR cycle for your change.
### Step 5: Decide What to Keep
After the change, decide which characterization tests to keep:
- **Keep** tests that document important behavior
- **Replace** tests that covered the code you changed (your TDD tests are better)
- **Remove** tests that only existed to enable your refactoring
---
## TDD by Test Type
| Test Type | TDD Approach |
|-----------|-------------|
| **Unit tests** | Standard RED-GREEN-REFACTOR. One behavior per cycle. |
| **Integration tests** | Write the test against the integration boundary first. May need stubs for external services during RED phase. |
| **API tests** | Define the request and expected response first. Implement handler to make it pass. |
| **E2E tests** | Not typically TDD'd per-cycle. Write E2E tests for critical paths after unit/integration TDD. |
---
## Quick Checklist
Before claiming a task is done with TDD:
- [ ] Every production function has at least one test that was written before the function
- [ ] No test was written after the code it tests (except characterization tests for legacy code)
- [ ] All tests pass
- [ ] Code has been refactored after going GREEN
- [ ] Tests verify behavior, not implementation
@@ -1,9 +1,11 @@
---
name: testing-anti-patterns
description: >
Trigger this skill whenever writing, reviewing, or debugging tests. Activate for keywords like "mock", "stub", "test helper", "flaky test", "test passes but bug ships", "false positive", "test coverage", or when tests seem unreliable. Also trigger when reviewing test code in PRs, when tests pass but production breaks, when someone proposes heavy mocking, or when test failures are intermittent. If any test smells wrong or feels like it is not actually verifying real behavior, this skill applies.
---
# Testing Anti-Patterns
## Description
Common testing mistakes that create false confidence in test suites. Learn to recognize and avoid these patterns that make tests pass while failing to verify actual behavior.
## When to Use
- Writing new tests
@@ -11,6 +13,12 @@ Common testing mistakes that create false confidence in test suites. Learn to re
- Debugging flaky or unreliable tests
- When tests pass but bugs still ship
## When NOT to Use
- Writing production code that is not test-related
- Test framework configuration or setup (e.g., jest.config, vitest.config)
- Performance testing or load testing scenarios
---
## The Five Anti-Patterns
@@ -256,3 +264,9 @@ Mocks should never:
- Create false confidence
---
## Related Skills
- `methodology/test-driven-development` -- TDD naturally prevents most testing anti-patterns by requiring tests before implementation
- `testing/pytest` -- Python-specific testing best practices that complement anti-pattern awareness
- `testing/vitest` -- TypeScript/JavaScript-specific testing best practices that complement anti-pattern awareness
@@ -0,0 +1,183 @@
# Testing Anti-Pattern Catalog
Quick reference of common testing anti-patterns. Each entry includes: what it looks like, why it's a problem, and how to fix it.
---
## 1. The Ice Cream Cone
**Symptom:** Most tests are E2E or integration tests. Few or no unit tests. The test pyramid is inverted.
**Root cause:** Tests were written after the feature, following user flows instead of testing isolated logic. Or the code is tightly coupled, making unit tests hard to write.
**Impact:** Test suite is slow, brittle, and expensive to maintain. Failures are hard to diagnose because tests cover too much at once.
**Fix:** Refactor toward the test pyramid. Extract business logic into pure functions and unit test them. Reserve E2E tests for critical user flows only. Target ratio: 70% unit, 20% integration, 10% E2E.
---
## 2. The Mockery
**Symptom:** Tests mock so aggressively that they're testing the mocks, not the actual code. The thing under test has all its dependencies replaced.
**Root cause:** Code has too many dependencies, or the developer equates "isolated" with "mock everything."
**Impact:** Tests pass even when the real code is broken. Refactoring breaks every test because mocks are coupled to implementation details.
**Fix:** Only mock external boundaries (network, database, filesystem, clock). Use real implementations for internal collaborators. If you need too many mocks, the code has too many dependencies — refactor first.
---
## 3. The Slow Suite
**Symptom:** Test suite takes more than a few minutes to run. Developers skip tests locally and only run them in CI.
**Root cause:** Too many integration/E2E tests, tests that hit real databases or network, no test parallelization, expensive setup/teardown.
**Impact:** Developers stop running tests, bugs slip through, CI becomes a bottleneck.
**Fix:**
- Profile the suite to find the slowest tests
- Replace slow integration tests with fast unit tests where possible
- Use in-memory databases for integration tests
- Parallelize test execution
- Target: unit suite under 30 seconds, full suite under 5 minutes
---
## 4. The Flaky Test
**Symptom:** Test passes most of the time but fails unpredictably. Re-running usually makes it pass.
**Root cause:** Race conditions, time-dependent logic, shared mutable state between tests, reliance on external services, non-deterministic ordering.
**Impact:** Team loses trust in tests. "Oh that one's flaky" becomes an excuse to ignore real failures. CI results become meaningless.
**Fix:**
- Isolate the flaky test and run it 100 times to confirm flakiness
- Check for: shared state, date/time usage, async timing, test ordering
- Fix the root cause (don't just add retries)
- Quarantine truly unfixable flaky tests while investigating
---
## 5. The Assertion-Free Test
**Symptom:** Test runs code but doesn't assert anything meaningful. It only checks that no exception was thrown.
```python
# Bad — this tests nothing useful
def test_process_data():
process_data(sample_input) # No assertion
```
**Root cause:** Test was written to hit a coverage target rather than verify behavior.
**Impact:** False sense of security. Code "has tests" but bugs go undetected.
**Fix:** Every test must assert on the outcome. Ask: "What behavior am I verifying?" If you can't answer, the test isn't testing anything.
```python
# Good — asserts the actual behavior
def test_process_data_calculates_total():
result = process_data(sample_input)
assert result.total == 42.0
```
---
## 6. The Copy-Paste Test
**Symptom:** Test file has blocks of nearly identical code repeated with minor variations. Tests are long and look like each other.
**Root cause:** Developer tested a new case by copying an existing test and tweaking values instead of extracting a pattern.
**Impact:** Maintenance nightmare. A change to the interface requires updating dozens of near-identical tests. Easy to introduce subtle bugs in copies.
**Fix:** Use parameterized tests for variations on the same behavior:
```python
# Python — pytest.mark.parametrize
@pytest.mark.parametrize("input,expected", [
("hello", "HELLO"),
("", ""),
("123", "123"),
])
def test_to_upper(input, expected):
assert to_upper(input) == expected
```
```typescript
// TypeScript — test.each (vitest/jest)
test.each([
["hello", "HELLO"],
["", ""],
["123", "123"],
])("to_upper(%s) returns %s", (input, expected) => {
expect(toUpper(input)).toBe(expected);
});
```
---
## 7. The Time Bomb
**Symptom:** Test passes today but will fail on a future date, or fails on certain days/times (new year, month boundary, DST change, leap year).
**Root cause:** Test uses `Date.now()`, `new Date()`, or similar without controlling the clock. Assertions are hardcoded to specific dates.
**Impact:** Sudden failures on specific dates. CI breaks on January 1, or during DST transitions.
**Fix:** Always inject or mock the clock:
```python
# Python — freeze time
from freezegun import freeze_time
@freeze_time("2025-06-15T12:00:00Z")
def test_expiry_check():
assert is_expired(created_at="2025-06-14T12:00:00Z", ttl_hours=23)
```
```typescript
// TypeScript — vitest fake timers
vi.useFakeTimers();
vi.setSystemTime(new Date("2025-06-15T12:00:00Z"));
expect(isExpired(createdAt, 23)).toBe(true);
vi.useRealTimers();
```
---
## 8. The Hidden Dependency
**Symptom:** Test passes locally but fails in CI, or fails when run in isolation but passes as part of the full suite.
**Root cause:** Test depends on external state that isn't set up by the test itself: a running database, a file on disk, an environment variable, output from a previous test, or global state modified by another test.
**Impact:** Tests are order-dependent, environment-dependent, and unreliable. Debugging failures requires understanding the entire test suite's execution order.
**Fix:**
- Each test must set up and tear down its own state
- Use fixtures (pytest fixtures, beforeEach/afterEach) for shared setup
- Run tests in random order to catch hidden dependencies
```bash
pytest -p randomly # Python
vitest --sequence.shuffle # vitest
```
- Never rely on test execution order
---
## Quick Decision Table
| Symptom | Likely Anti-Pattern | First Action |
|---------|-------------------|--------------|
| Tests are slow | Ice Cream Cone or Slow Suite | Profile, find the slowest tests |
| Tests break on refactor | The Mockery | Reduce mocks, test behavior not implementation |
| Tests fail randomly | Flaky Test | Isolate and run 100x |
| High coverage but bugs slip through | Assertion-Free Test | Audit assertions in coverage-targeted tests |
| Tests are hard to maintain | Copy-Paste Test | Extract parameterized tests |
| Tests fail on certain dates | Time Bomb | Inject/mock the clock |
| Tests fail in CI only | Hidden Dependency | Run locally in random order |
| Tests pass but code is clearly broken | The Mockery or Assertion-Free | Check what's actually being asserted |
@@ -1,9 +1,11 @@
---
name: verification-before-completion
description: >
Trigger this skill whenever about to claim ANY work is complete, fixed, passing, or done. Activate whenever you are tempted to say "done", "fixed", "tests pass", "build succeeds", "deployed", or any completion claim. Also trigger before committing code, before creating PRs, before responding to the user that a task is finished, or when reviewing agent-produced work. This is mandatory -- NEVER claim completion without running verification commands and reading their output. Evidence before assertions, always.
---
# Verification Before Completion
## Description
Mandatory verification process before claiming any task is complete. This skill enforces evidence-based completion rather than assumption-based claims.
## When to Use
- Before claiming tests pass
@@ -12,6 +14,12 @@ Mandatory verification process before claiming any task is complete. This skill
- Before marking any task complete
- Before declaring success to user
## When NOT to Use
- Mid-task progress updates where you are reporting interim status, not claiming completion
- Research or exploration tasks where the output is knowledge, not code
- Design or brainstorming phases where no verifiable artifacts have been produced yet
---
## The 5-Step Verification Process
@@ -260,3 +268,9 @@ Use before claiming completion:
```
---
## Related Skills
- `methodology/test-driven-development` -- TDD naturally produces verifiable work; verification confirms the TDD cycle was followed correctly
- `methodology/systematic-debugging` -- After debugging, verification ensures the fix actually resolves the issue
- `methodology/requesting-code-review` -- Verification should happen before requesting review to avoid wasting reviewer time on broken code
@@ -0,0 +1,116 @@
# Verification Checklist
Use this checklist before claiming any work is complete. Copy it into your task, PR, or plan and fill in the specifics. Every box must be checked with evidence — not assumptions.
---
## Core Verification
### Tests
- [ ] **All existing tests pass**
- Command: `___`
- Output: [paste summary or confirm "all N tests passed"]
- [ ] **New tests added for new behavior**
- Test files: `___`
- Coverage of changed code: `___`%
- [ ] **Edge cases tested**
- [ ] Empty/null inputs
- [ ] Boundary values
- [ ] Error conditions
- [ ] Concurrent access (if applicable)
### Build
- [ ] **Build succeeds with no errors**
- Command: `___`
- Output: [confirm clean build]
- [ ] **No new warnings introduced**
- Linter: `___`
- Type checker: `___`
### Manual Verification
- [ ] **The specific change works as intended**
- What I did: [exact steps taken]
- What I observed: [exact result]
- What was expected: [matches requirement]
- [ ] **Related functionality still works**
- Checked: [list related features tested]
---
## Safety Checks
### No Unintended Side Effects
- [ ] **Reviewed the full diff** — No accidental changes to unrelated files
```bash
git diff --stat
```
- [ ] **No debug code left in place** — No `console.log`, `print()`, `debugger`, `TODO: remove`
- [ ] **No commented-out code** — Either the code is needed or it isn't
### Error Handling
- [ ] **Errors produce clear messages** — Not generic "something went wrong"
- [ ] **Errors don't leak sensitive information** — No stack traces, internal paths, or credentials in user-facing errors
- [ ] **Failure modes are graceful** — The system degrades rather than crashes
### Security
- [ ] **No hardcoded secrets** — API keys, passwords, tokens are in environment variables
- [ ] **Input is validated** — User input is checked before processing
- [ ] **Output is encoded** — Rendered content is escaped appropriately
- [ ] **No new `eval()` or dynamic code execution**
- [ ] **Dependencies are from trusted sources** — No typosquatting, pinned versions
---
## Documentation
- [ ] **Code is self-documenting** — Clear names, obvious structure
- [ ] **Complex logic has comments** — Explaining WHY, not WHAT
- [ ] **Public API changes are documented** — Updated docstrings, OpenAPI specs, README
- [ ] **Breaking changes are called out** — In commit message, PR description, or changelog
---
## Completion Criteria
- [ ] **All acceptance criteria from the plan/ticket are met**
- [ ] Criterion 1: ___
- [ ] Criterion 2: ___
- [ ] **The change has been verified with actual commands, not just code reading**
- [ ] **Confidence level:** High / Medium / Low
- If Medium or Low, explain what additional verification would increase confidence: ___
---
## Evidence Summary
Record the key evidence here for reviewers:
| Check | Evidence |
|-------|---------|
| Tests pass | [command + result] |
| Build clean | [command + result] |
| Manual test | [steps + result] |
| No regressions | [how verified] |
---
## Usage Notes
- **Do not check boxes without evidence.** "I think it works" is not verification.
- **Run commands and observe output.** Paste or summarize actual results.
- **N/A is acceptable** for items that genuinely don't apply, but add a note explaining why.
- **If confidence is low**, list what would increase it and discuss with the team before marking complete.
@@ -1,9 +1,11 @@
---
name: writing-plans
description: >
Trigger this skill whenever a multi-step implementation task needs to be broken down before coding begins. Activate for keywords like "plan", "break down", "implementation steps", "task list", "how to implement", "write a plan", or when a feature spans multiple files or components. Also trigger when handing off work to another developer, when the user says "let's plan this out", or when a task is complex enough that jumping straight to code would be risky. If in doubt, plan first.
---
# Writing Plans
## Description
Generate comprehensive implementation plans with bite-sized tasks for engineers with minimal codebase familiarity. This skill bridges design completion and engineering execution with detailed, actionable steps.
## When to Use
- After brainstorming/design is complete
@@ -11,6 +13,12 @@ Generate comprehensive implementation plans with bite-sized tasks for engineers
- When handing off work to another developer
- For complex features requiring structured approach
## When NOT to Use
- Single-file changes where the path forward is obvious
- Already has a plan to execute -- use `executing-plans` instead
- Exploration or research tasks where the goal is learning, not building
---
## Plan Document Format
@@ -275,3 +283,9 @@ mark user as verified in database.
```
---
## Related Skills
- `methodology/brainstorming` -- Use before writing plans when requirements are unclear or need exploration
- `methodology/executing-plans` -- Use after writing a plan to execute it with subagent-driven development and review gates
- `methodology/test-driven-development` -- Plans follow TDD principles; reference this skill for strict red-green-refactor enforcement
@@ -0,0 +1,147 @@
# [Feature Name] Implementation Plan
> **Author:** [name]
> **Date:** [date]
> **Status:** Draft | In Review | Approved | In Progress | Complete
> **Estimated Total:** [X hours/days]
---
## Context
### Problem Statement
[One paragraph describing what problem this solves and why it matters now.]
### Background
[Any relevant context: prior decisions, related features, technical debt involved.]
### Goals
- [Primary goal]
- [Secondary goal]
### Non-Goals
- [What this plan explicitly does NOT address]
---
## Tasks
### Task 1: [Name] (estimated: Xmin)
**Description:** [What this task accomplishes and why it's needed.]
**Files to modify:**
- `path/to/file.ts` — [what changes]
- `path/to/other.py` — [what changes]
**New files:**
- `path/to/new-file.ts` — [purpose]
**Changes:**
1. [Specific change with enough detail to implement without ambiguity]
2. [Next change]
**Tests:**
- [ ] [Test description — what behavior is verified]
- [ ] [Edge case test]
**Verification:**
- [ ] [How to verify this task is complete — command to run, behavior to observe]
---
### Task 2: [Name] (estimated: Xmin)
**Description:** [What this task accomplishes.]
**Dependencies:** Task 1 (requires [specific output])
**Files to modify:**
- `path/to/file.ts` — [what changes]
**Changes:**
1. [Specific change]
2. [Next change]
**Tests:**
- [ ] [Test description]
**Verification:**
- [ ] [Verification step]
---
### Task 3: [Name] (estimated: Xmin)
[Repeat the same structure. Add as many tasks as needed.]
---
## Dependencies
### Internal Dependencies
| Task | Depends On | Reason |
|------|-----------|--------|
| Task 2 | Task 1 | [Why] |
### External Dependencies
- [ ] [External service, API key, environment setup, etc.]
- [ ] [Approval or decision needed from someone]
### Parallel Work
[Which tasks can be done simultaneously? Group them.]
- **Group A (independent):** Task 1, Task 3
- **Group B (requires Group A):** Task 2, Task 4
---
## Risks
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| [What could go wrong] | Low/Med/High | Low/Med/High | [How to prevent or handle it] |
---
## Verification Plan
### Automated Checks
```bash
# Run full test suite
[test command]
# Run type checks
[type check command]
# Run linter
[lint command]
```
### Manual Checks
- [ ] [Specific scenario to test manually]
- [ ] [Edge case to verify by hand]
### Acceptance Criteria
- [ ] [Criterion 1 — ties back to Goals section]
- [ ] [Criterion 2]
---
## Notes
[Any additional context, open questions, or decisions to revisit.]
---
## Usage Instructions
**To use this template:**
1. Copy this file and rename it: `plan-[feature-name].md`
2. Fill in all sections. If a section doesn't apply, write "N/A" rather than deleting it
3. For **standard plans**: tasks should be 15-60 minutes each
4. For **detailed plans** (`--detailed`): tasks should be 2-5 minutes with exact code snippets
5. Every task must have at least one verification step
6. Every task must list the specific files it touches
7. Remove these usage instructions from your copy
@@ -1,9 +1,11 @@
---
name: token-efficient
description: >
Use this skill when optimizing token usage, reducing response verbosity, or working in high-volume development sessions. Trigger for any mention of token savings, cost optimization, concise output, compressed responses, or the --format=concise/ultra flags. Also applies during repetitive tasks, quick iterations, simple clear requests, or when the user activates token-efficient mode. This is a cross-cutting optimization that applies to all other skills.
---
# Token Optimization
## Description
Patterns and techniques for reducing token usage while maintaining response quality. Achieve 30-70% cost savings through strategic output compression.
## When to Use
- High-volume development sessions
@@ -12,6 +14,12 @@ Patterns and techniques for reducing token usage while maintaining response qual
- Cost-sensitive projects
- Quick iterations
## When NOT to Use
- Learning or educational contexts where verbose explanations help the user understand concepts
- Debugging complex issues where detailed analysis and step-by-step reasoning matter
- Security reviews or architecture discussions where thoroughness is more important than brevity
---
## Compression Levels
@@ -196,3 +204,7 @@ Monthly savings: ~7.5M tokens
- Review needed → Full output
---
## Related Skills
- All skills — this is a cross-cutting optimization that can be combined with any other skill to reduce token usage while maintaining response quality
+772
View File
@@ -0,0 +1,772 @@
---
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
## When to Use
- Setting up HTTP clients (axios, fetch, httpx) for consuming external REST APIs
- Adding request/response interceptors for logging, auth tokens, or error transformation
- Implementing retry logic with exponential backoff for transient failures
- Generating type-safe API clients from OpenAPI or Swagger specifications
- Managing authentication tokens (Bearer injection, auto-refresh on 401)
## When NOT to Use
- Internal function calls or in-process service communication that does not cross a network boundary
- Database queries -- use an ORM or database driver instead
- WebSocket or real-time streaming connections -- use dedicated WebSocket client patterns
- GraphQL clients -- use a GraphQL-specific library such as Apollo or urql
---
## Core Patterns
### 1. HTTP Client Setup
Create a single, pre-configured client instance per external service. Never scatter raw `fetch()` or `requests.get()` calls throughout the codebase.
**Python -- httpx (async)**
```python
# BAD - creating a new client on every call, no shared config
import httpx
async def get_user(user_id: int):
response = httpx.get(f"https://api.example.com/users/{user_id}") # no timeout, no auth
return response.json()
# GOOD - shared async client with base URL, timeout, and headers
import httpx
class ApiClient:
def __init__(self, base_url: str, api_key: str):
self._client = httpx.AsyncClient(
base_url=base_url,
headers={
"Authorization": f"Bearer {api_key}",
"Accept": "application/json",
"User-Agent": "myapp/1.0",
},
timeout=httpx.Timeout(10.0, connect=5.0),
)
async def get_user(self, user_id: int) -> dict:
response = await self._client.get(f"/users/{user_id}")
response.raise_for_status()
return response.json()
async def close(self):
await self._client.aclose()
```
**Python -- httpx (sync)**
```python
# GOOD - synchronous client for scripts, CLIs, or sync frameworks
import httpx
client = httpx.Client(
base_url="https://api.example.com",
headers={"Authorization": f"Bearer {api_key}"},
timeout=httpx.Timeout(10.0, connect=5.0),
)
def get_user(user_id: int) -> dict:
response = client.get(f"/users/{user_id}")
response.raise_for_status()
return response.json()
```
**TypeScript -- fetch wrapper**
```typescript
// BAD - raw fetch with no error handling or shared config
const res = await fetch("https://api.example.com/users/1");
const data = await res.json();
// GOOD - typed fetch wrapper with defaults
interface RequestConfig extends RequestInit {
params?: Record<string, string>;
}
class ApiClient {
constructor(
private baseUrl: string,
private defaultHeaders: Record<string, string> = {},
) {}
private async request<T>(path: string, config: RequestConfig = {}): Promise<T> {
const url = new URL(path, this.baseUrl);
if (config.params) {
Object.entries(config.params).forEach(([k, v]) => url.searchParams.set(k, v));
}
const response = await fetch(url.toString(), {
...config,
headers: { ...this.defaultHeaders, ...config.headers },
});
if (!response.ok) {
throw new ApiError(response.status, await response.text());
}
return response.json() as Promise<T>;
}
get<T>(path: string, config?: RequestConfig): Promise<T> {
return this.request<T>(path, { ...config, method: "GET" });
}
post<T>(path: string, body: unknown, config?: RequestConfig): Promise<T> {
return this.request<T>(path, {
...config,
method: "POST",
body: JSON.stringify(body),
headers: { "Content-Type": "application/json", ...config?.headers },
});
}
}
const api = new ApiClient("https://api.example.com", {
Authorization: `Bearer ${process.env.API_KEY}`,
Accept: "application/json",
});
```
**TypeScript -- axios instance**
```typescript
// GOOD - axios instance with shared config
import axios from "axios";
const api = axios.create({
baseURL: "https://api.example.com",
timeout: 10_000,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
});
// All requests share the same base URL, timeout, and headers
const user = await api.get<User>("/users/1");
const created = await api.post<User>("/users", { name: "Alice" });
```
### 2. Request/Response Interceptors
Interceptors centralize cross-cutting concerns so individual API calls stay clean.
**Axios interceptors (TypeScript)**
```typescript
// GOOD - auth token injection
api.interceptors.request.use((config) => {
const token = tokenStore.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// GOOD - request/response logging
api.interceptors.request.use((config) => {
console.debug(`[API] ${config.method?.toUpperCase()} ${config.url}`);
return config;
});
api.interceptors.response.use(
(response) => {
console.debug(`[API] ${response.status} ${response.config.url}`);
return response;
},
(error) => {
console.error(`[API] Error ${error.response?.status} ${error.config?.url}`);
return Promise.reject(error);
},
);
// GOOD - error transformation to application-specific errors
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response) {
const { status, data } = error.response;
throw new ApiError(status, data?.message ?? "Unknown API error", data?.code);
}
if (error.code === "ECONNABORTED") {
throw new TimeoutError(`Request timed out: ${error.config?.url}`);
}
throw new NetworkError("Network connection failed");
},
);
```
**httpx event hooks (Python)**
```python
# GOOD - logging and error hooks on httpx client
import httpx
import logging
logger = logging.getLogger("api_client")
async def log_request(request: httpx.Request):
logger.debug(f"Request: {request.method} {request.url}")
async def log_response(response: httpx.Response):
logger.debug(f"Response: {response.status_code} {response.url}")
async def raise_on_error(response: httpx.Response):
if response.status_code >= 400:
await response.aread()
logger.error(f"API error {response.status_code}: {response.text[:200]}")
client = httpx.AsyncClient(
base_url="https://api.example.com",
event_hooks={
"request": [log_request],
"response": [log_response, raise_on_error],
},
)
```
### 3. Retry Logic
Retry transient failures with exponential backoff. Never retry non-idempotent requests blindly.
**Python -- tenacity**
```python
# GOOD - retry with exponential backoff for specific status codes
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception
import httpx
def is_retryable(exc: BaseException) -> bool:
if isinstance(exc, httpx.HTTPStatusError):
return exc.response.status_code in (429, 500, 502, 503, 504)
if isinstance(exc, (httpx.ConnectTimeout, httpx.ReadTimeout)):
return True
return False
@retry(
retry=retry_if_exception(is_retryable),
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10),
reraise=True,
)
async def fetch_with_retry(client: httpx.AsyncClient, url: str) -> dict:
response = await client.get(url)
response.raise_for_status()
return response.json()
```
**Python -- manual retry with Retry-After**
```python
# GOOD - respects Retry-After header from rate-limited responses
import asyncio
import httpx
async def fetch_respecting_rate_limit(
client: httpx.AsyncClient,
url: str,
max_retries: int = 3,
) -> httpx.Response:
for attempt in range(max_retries):
response = await client.get(url)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 2 ** attempt))
await asyncio.sleep(min(retry_after, 60))
continue
response.raise_for_status()
return response
raise httpx.HTTPStatusError(
"Max retries exceeded", request=response.request, response=response
)
```
**TypeScript -- custom retry wrapper**
```typescript
// GOOD - generic retry wrapper with exponential backoff
interface RetryOptions {
maxRetries: number;
baseDelay: number;
maxDelay: number;
retryableStatuses: number[];
}
const DEFAULT_RETRY: RetryOptions = {
maxRetries: 3,
baseDelay: 1000,
maxDelay: 10000,
retryableStatuses: [429, 500, 502, 503, 504],
};
async function withRetry<T>(
fn: () => Promise<T>,
options: Partial<RetryOptions> = {},
): Promise<T> {
const opts = { ...DEFAULT_RETRY, ...options };
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
const status = error instanceof ApiError ? error.status : 0;
const isRetryable = opts.retryableStatuses.includes(status);
const isLastAttempt = attempt === opts.maxRetries;
if (!isRetryable || isLastAttempt) throw error;
const delay = Math.min(opts.baseDelay * 2 ** attempt, opts.maxDelay);
const jitter = delay * (0.5 + Math.random() * 0.5);
await new Promise((resolve) => setTimeout(resolve, jitter));
}
}
throw new Error("Unreachable");
}
// Usage
const user = await withRetry(() => api.get<User>("/users/1"));
```
### 4. Type-Safe Clients from OpenAPI
Generate clients from OpenAPI specs to eliminate hand-written API types and reduce drift between backend and frontend.
**TypeScript -- openapi-typescript + openapi-fetch**
```bash
# Generate types from an OpenAPI spec
npx openapi-typescript https://api.example.com/openapi.json -o src/api/schema.d.ts
```
```typescript
// GOOD - fully typed client from generated schema
import createClient from "openapi-fetch";
import type { paths } from "./schema";
const api = createClient<paths>({
baseUrl: "https://api.example.com",
headers: { Authorization: `Bearer ${token}` },
});
// Paths, methods, params, and response types are all inferred
const { data, error } = await api.GET("/users/{id}", {
params: { path: { id: 42 } },
});
// data is typed as the 200 response schema
// error is typed as the error response schema
```
**TypeScript -- zodios (Zod + axios)**
```typescript
// GOOD - runtime-validated API client with Zod schemas
import { makeApi, Zodios } from "@zodios/core";
import { z } from "zod";
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
const api = makeApi([
{
method: "get",
path: "/users/:id",
alias: "getUser",
response: userSchema,
},
{
method: "post",
path: "/users",
alias: "createUser",
parameters: [{ name: "body", type: "Body", schema: userSchema.omit({ id: true }) }],
response: userSchema,
},
]);
const client = new Zodios("https://api.example.com", api);
// Fully typed and runtime validated
const user = await client.getUser({ params: { id: 42 } });
```
**Python -- datamodel-code-generator**
```bash
# Generate Pydantic models from an OpenAPI spec
pip install datamodel-code-generator
datamodel-codegen --input openapi.json --output src/api/models.py --input-file-type openapi
```
```python
# Generated models are Pydantic BaseModel classes
from api.models import User, CreateUserRequest
# Use them with httpx for typed requests
async def create_user(client: httpx.AsyncClient, payload: CreateUserRequest) -> User:
response = await client.post("/users", json=payload.model_dump())
response.raise_for_status()
return User.model_validate(response.json())
```
### 5. Authentication
Centralize auth token management so every request gets the right credentials without per-call boilerplate.
**Bearer token injection (axios)**
```typescript
// GOOD - automatic token refresh on 401
let isRefreshing = false;
let failedQueue: Array<{
resolve: (token: string) => void;
reject: (error: unknown) => void;
}> = [];
function processQueue(error: unknown, token: string | null) {
failedQueue.forEach(({ resolve, reject }) => {
if (error) reject(error);
else resolve(token!);
});
failedQueue = [];
}
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status !== 401 || originalRequest._retry) {
return Promise.reject(error);
}
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return api(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const { data } = await axios.post("/auth/refresh", {
refreshToken: tokenStore.getRefreshToken(),
});
tokenStore.setAccessToken(data.accessToken);
processQueue(null, data.accessToken);
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
return api(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
tokenStore.clear();
window.location.href = "/login";
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
},
);
```
**Python -- httpx auth class**
```python
# GOOD - custom auth flow with automatic refresh
import httpx
import time
class BearerAuth(httpx.Auth):
def __init__(self, token_url: str, client_id: str, client_secret: str):
self.token_url = token_url
self.client_id = client_id
self.client_secret = client_secret
self._access_token: str | None = None
self._expires_at: float = 0
def auth_flow(self, request: httpx.Request):
if self._is_expired():
token_response = yield self._build_token_request()
token_response.raise_for_status()
data = token_response.json()
self._access_token = data["access_token"]
self._expires_at = time.time() + data["expires_in"] - 30 # 30s buffer
request.headers["Authorization"] = f"Bearer {self._access_token}"
yield request
def _is_expired(self) -> bool:
return self._access_token is None or time.time() >= self._expires_at
def _build_token_request(self) -> httpx.Request:
return httpx.Request(
"POST",
self.token_url,
data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
},
)
# Usage
auth = BearerAuth(
token_url="https://auth.example.com/token",
client_id=os.environ["CLIENT_ID"],
client_secret=os.environ["CLIENT_SECRET"],
)
client = httpx.AsyncClient(base_url="https://api.example.com", auth=auth)
```
**API key via custom header**
```python
# GOOD - API key as header, loaded from environment
import os
import httpx
client = httpx.AsyncClient(
base_url="https://api.example.com",
headers={"X-API-Key": os.environ["EXAMPLE_API_KEY"]},
)
```
### 6. Error Handling
Distinguish between network errors, timeout errors, and API-level errors. Never swallow exceptions silently.
**TypeScript -- structured error classes**
```typescript
// GOOD - error hierarchy for API calls
class ApiError extends Error {
constructor(
public readonly status: number,
public readonly body: string,
public readonly code?: string,
) {
super(`API error ${status}: ${body.slice(0, 200)}`);
this.name = "ApiError";
}
get isClientError(): boolean {
return this.status >= 400 && this.status < 500;
}
get isServerError(): boolean {
return this.status >= 500;
}
}
class NetworkError extends Error {
constructor(message: string, public readonly cause?: Error) {
super(message);
this.name = "NetworkError";
}
}
class TimeoutError extends Error {
constructor(message: string) {
super(message);
this.name = "TimeoutError";
}
}
```
**Timeout and cancellation with AbortController**
```typescript
// GOOD - cancel requests that take too long or on component unmount
async function fetchWithTimeout<T>(url: string, timeoutMs: number = 5000): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new ApiError(response.status, await response.text());
return response.json() as Promise<T>;
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") {
throw new TimeoutError(`Request to ${url} timed out after ${timeoutMs}ms`);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
// React -- cancel on unmount
function useApiData(url: string) {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then((res) => res.json())
.then(setData)
.catch((err) => {
if (err.name !== "AbortError") console.error(err);
});
return () => controller.abort();
}, [url]);
return data;
}
```
**Python -- structured error handling**
```python
# GOOD - catch specific httpx exceptions
import httpx
async def safe_api_call(client: httpx.AsyncClient, path: str) -> dict | None:
try:
response = await client.get(path)
response.raise_for_status()
return response.json()
except httpx.ConnectTimeout:
logger.error(f"Connection timeout: {path}")
raise
except httpx.ReadTimeout:
logger.error(f"Read timeout: {path}")
raise
except httpx.HTTPStatusError as exc:
logger.error(f"HTTP {exc.response.status_code} from {path}: {exc.response.text[:200]}")
if exc.response.status_code == 404:
return None
raise
except httpx.ConnectError:
logger.error(f"Connection failed: {path}")
raise
```
### 7. Rate Limiting (Client-Side)
Respect API rate limits to avoid being throttled or banned.
**TypeScript -- request queue with concurrency control**
```typescript
// GOOD - throttle outgoing requests to stay under rate limits
class RequestQueue {
private queue: Array<() => Promise<void>> = [];
private running = 0;
constructor(
private maxConcurrent: number = 5,
private minDelay: number = 100,
) {}
async add<T>(fn: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try {
resolve(await fn());
} catch (error) {
reject(error);
}
});
this.process();
});
}
private async process(): Promise<void> {
if (this.running >= this.maxConcurrent || this.queue.length === 0) return;
this.running++;
const task = this.queue.shift()!;
await task();
await new Promise((resolve) => setTimeout(resolve, this.minDelay));
this.running--;
this.process();
}
}
// Usage -- at most 5 concurrent requests, 100ms between each
const queue = new RequestQueue(5, 100);
const users = await Promise.all(
userIds.map((id) => queue.add(() => api.get<User>(`/users/${id}`))),
);
```
**Python -- asyncio semaphore throttle**
```python
# GOOD - limit concurrent requests with a semaphore
import asyncio
import httpx
class ThrottledClient:
def __init__(self, client: httpx.AsyncClient, max_concurrent: int = 5):
self._client = client
self._semaphore = asyncio.Semaphore(max_concurrent)
async def get(self, url: str, **kwargs) -> httpx.Response:
async with self._semaphore:
response = await self._client.get(url, **kwargs)
# Respect Retry-After if rate limited
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", "5"))
await asyncio.sleep(retry_after)
response = await self._client.get(url, **kwargs)
return response
```
---
## Best Practices
1. **Create one client instance per external service.** Share it across your application. Instantiating new clients on every call wastes connections and prevents connection pooling.
2. **Always set explicit timeouts.** A missing timeout means a stuck request can hang your entire application. Set both connect and read timeouts. Five to ten seconds is a sensible default for most APIs.
3. **Centralize error handling in interceptors or middleware.** Do not scatter try/catch blocks around every individual API call. Use interceptors to transform HTTP errors into typed application errors.
4. **Add jitter to retry backoff.** Pure exponential backoff causes thundering herd problems when many clients retry simultaneously. Add random jitter to spread retries across time.
5. **Never retry non-idempotent requests automatically.** POST requests that create resources can cause duplicates if retried blindly. Only retry GET, HEAD, and PUT (idempotent methods) by default.
6. **Generate types from OpenAPI specs instead of writing them by hand.** This eliminates drift between backend and frontend types and reduces maintenance effort.
7. **Log request and response metadata, not bodies.** Log method, URL, status code, and duration. Avoid logging request or response bodies by default -- they may contain sensitive data like tokens or PII.
8. **Close clients when the application shuts down.** In Python, use `async with` or call `aclose()`. In Node.js, use AbortController or connection pool shutdown. Leaked connections cause resource exhaustion.
---
## Common Pitfalls
1. **Not closing httpx clients.** Failing to call `aclose()` leaks connections and file descriptors. Use `async with httpx.AsyncClient() as client:` or register a shutdown handler.
2. **Storing API keys in source code.** Always load secrets from environment variables or a secret manager. Never commit API keys, tokens, or credentials to version control.
3. **Ignoring response status codes.** `fetch()` does not throw on 4xx/5xx -- you must check `response.ok` or call `.raise_for_status()`. This is the most common fetch mistake.
4. **Retrying 400-level errors.** Client errors (400, 401, 403, 404, 422) are not transient. Retrying them wastes time and load. Only retry on 429 (rate limit) and 5xx (server errors).
5. **Building URLs with string concatenation.** Concatenating user input into URLs creates injection risks and encoding bugs. Use `URL` constructor (JS) or `httpx.URL` (Python) for safe URL building.
6. **Not cancelling requests on component unmount.** In React, fetch requests that complete after unmount cause state-update-on-unmounted-component warnings and potential memory leaks. Always use AbortController with a cleanup function.
---
## 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
@@ -0,0 +1,250 @@
# HTTP Client Patterns Quick Reference
## Python HTTP Clients
| Feature | httpx | requests | aiohttp |
|---------|-------|----------|---------|
| Async support | Yes (native) | No | Yes (async-only) |
| HTTP/2 | Yes | No | No |
| Connection pooling | Yes | Yes (Session) | Yes |
| Streaming | Yes | Yes | Yes |
| Type hints | Yes | Partial | Partial |
| Timeout default | No timeout | No timeout | 5 min |
| Recommended for | Modern projects | Simple scripts | Legacy async |
### httpx Setup (Recommended)
```python
import httpx
# Sync client with defaults
client = httpx.Client(
base_url="https://api.example.com",
timeout=httpx.Timeout(10.0, connect=5.0),
headers={"Authorization": f"Bearer {token}"},
)
# Async client
async_client = httpx.AsyncClient(
base_url="https://api.example.com",
timeout=10.0,
http2=True,
)
# Always use as context manager (ensures cleanup)
async with httpx.AsyncClient() as client:
response = await client.get("/users")
```
### httpx Retry Pattern
```python
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10),
retry=retry_if_exception_type((httpx.TimeoutException, httpx.NetworkError)),
)
async def fetch_with_retry(client: httpx.AsyncClient, url: str) -> dict:
response = await client.get(url)
response.raise_for_status()
return response.json()
```
### httpx Interceptor Pattern (Event Hooks)
```python
def log_request(request: httpx.Request):
print(f"--> {request.method} {request.url}")
def log_response(response: httpx.Response):
print(f"<-- {response.status_code} {response.url} ({response.elapsed.total_seconds():.2f}s)")
def raise_on_error(response: httpx.Response):
response.raise_for_status()
client = httpx.AsyncClient(
event_hooks={
"request": [log_request],
"response": [log_response, raise_on_error],
}
)
```
---
## JavaScript/TypeScript HTTP Clients
| Feature | fetch (native) | axios | ky |
|---------|---------------|-------|-----|
| Built-in | Yes | No (~13KB) | No (~3KB) |
| Interceptors | No (manual) | Yes | Yes (hooks) |
| Auto JSON | No (manual `.json()`) | Yes | Yes |
| Timeout | AbortSignal.timeout() | Built-in | Built-in |
| Retry | No | No (plugin) | Built-in |
| Cancel | AbortController | CancelToken (deprecated) / AbortController | AbortController |
| Streaming | Yes (ReadableStream) | Node only | Yes |
| Recommended for | Simple needs, SSR | Large existing codebases | Modern projects |
### fetch Wrapper Pattern
```typescript
class ApiClient {
constructor(
private baseUrl: string,
private defaultHeaders: Record<string, string> = {}
) {}
private async request<T>(path: string, init?: RequestInit): Promise<T> {
const url = `${this.baseUrl}${path}`;
const response = await fetch(url, {
...init,
headers: { "Content-Type": "application/json", ...this.defaultHeaders, ...init?.headers },
signal: init?.signal ?? AbortSignal.timeout(10_000),
});
if (!response.ok) {
const body = await response.text();
throw new ApiError(response.status, body, url);
}
return response.json();
}
get<T>(path: string, signal?: AbortSignal) {
return this.request<T>(path, { signal });
}
post<T>(path: string, data: unknown) {
return this.request<T>(path, { method: "POST", body: JSON.stringify(data) });
}
put<T>(path: string, data: unknown) {
return this.request<T>(path, { method: "PUT", body: JSON.stringify(data) });
}
delete<T>(path: string) {
return this.request<T>(path, { method: "DELETE" });
}
}
```
### ky Setup (Recommended for JS)
```typescript
import ky from "ky";
const api = ky.create({
prefixUrl: "https://api.example.com",
timeout: 10_000,
retry: { limit: 3, methods: ["get"], statusCodes: [408, 429, 500, 502, 503] },
hooks: {
beforeRequest: [
(request) => {
request.headers.set("Authorization", `Bearer ${getToken()}`);
},
],
afterResponse: [
async (_request, _options, response) => {
if (response.status === 401) {
await refreshToken();
// ky will retry automatically
}
},
],
},
});
// Usage
const users = await api.get("users").json<User[]>();
const user = await api.post("users", { json: { name: "Alice" } }).json<User>();
```
---
## Error Handling Patterns
### Typed Error Class
```typescript
class ApiError extends Error {
constructor(
public status: number,
public body: string,
public url: string,
) {
super(`HTTP ${status} from ${url}`);
this.name = "ApiError";
}
get isRetryable(): boolean {
return this.status >= 500 || this.status === 429;
}
get isAuthError(): boolean {
return this.status === 401;
}
}
```
```python
class ApiError(Exception):
def __init__(self, status: int, body: str, url: str):
self.status = status
self.body = body
self.url = url
super().__init__(f"HTTP {status} from {url}")
@property
def is_retryable(self) -> bool:
return self.status >= 500 or self.status == 429
```
### Error Handling Decision
| Status | Action |
|--------|--------|
| 400 | Don't retry. Fix the request. Log validation details. |
| 401 | Refresh token and retry once. If still 401, re-authenticate. |
| 403 | Don't retry. User lacks permission. |
| 404 | Don't retry. Resource doesn't exist. |
| 408, 429 | Retry with backoff. Respect `Retry-After` header. |
| 500-503 | Retry with exponential backoff (max 3 attempts). |
| Network error | Retry with backoff. Check connectivity. |
| Timeout | Retry with longer timeout or fail fast. |
---
## Auth Token Refresh Pattern
```typescript
let refreshPromise: Promise<string> | null = null;
async function getValidToken(): Promise<string> {
const token = getStoredToken();
if (!isExpired(token)) return token;
// Deduplicate concurrent refresh calls
if (!refreshPromise) {
refreshPromise = refreshToken().finally(() => { refreshPromise = null; });
}
return refreshPromise;
}
```
---
## Quick Setup Checklist
| Concern | Implementation |
|---------|---------------|
| Base URL | Configure once in client factory |
| Auth header | Interceptor / hook (not per-request) |
| Timeout | Always set (10s default, 30s for uploads) |
| Retry | 3 attempts, exponential backoff, only GET + idempotent |
| Error handling | Typed errors, status-based decisions |
| Cancellation | AbortController (pass signal to all requests) |
| Logging | Log method, URL, status, duration (not bodies in prod) |
| Content-Type | Set `application/json` as default, override for file uploads |
@@ -0,0 +1,859 @@
---
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 & Authorization Patterns
## When to Use
- Implementing login, signup, or logout flows for web applications
- Setting up JWT access tokens and refresh token rotation
- Building OAuth2 integrations (Google, GitHub, or custom providers)
- Adding role-based or permission-based access control to API endpoints
- Protecting routes with middleware guards in Next.js, Express, or FastAPI
## When NOT to Use
- Public-only APIs that require no identity verification (e.g., open data endpoints)
- Internal services secured entirely at the network level (VPC, service mesh mTLS) with no application-layer auth
- Static sites with no user-specific content or server-side logic
---
## Core Patterns
### 1. JWT Patterns
Use short-lived access tokens for API authorization and long-lived refresh tokens for session continuity. Never store access tokens in localStorage.
**Token structure and signing**
```python
# BAD - long-lived token, weak secret, symmetric HS256 with hardcoded key
import jwt
token = jwt.encode(
{"user_id": 1, "exp": datetime.utcnow() + timedelta(days=365)},
"secret123",
algorithm="HS256",
)
# GOOD - short-lived access token, strong secret from env, RS256 for production
import jwt
import os
from datetime import datetime, timedelta, timezone
ACCESS_TOKEN_EXPIRY = timedelta(minutes=15)
REFRESH_TOKEN_EXPIRY = timedelta(days=7)
def create_access_token(user_id: int, role: str) -> str:
now = datetime.now(timezone.utc)
return jwt.encode(
{
"sub": str(user_id),
"role": role,
"iat": now,
"exp": now + ACCESS_TOKEN_EXPIRY,
"type": "access",
},
os.environ["JWT_PRIVATE_KEY"],
algorithm="RS256",
)
def create_refresh_token(user_id: int) -> str:
now = datetime.now(timezone.utc)
return jwt.encode(
{
"sub": str(user_id),
"iat": now,
"exp": now + REFRESH_TOKEN_EXPIRY,
"type": "refresh",
"jti": str(uuid.uuid4()), # unique ID for revocation
},
os.environ["JWT_PRIVATE_KEY"],
algorithm="RS256",
)
def decode_token(token: str) -> dict:
return jwt.decode(
token,
os.environ["JWT_PUBLIC_KEY"],
algorithms=["RS256"],
options={"require": ["sub", "exp", "type"]},
)
```
**TypeScript -- JWT creation and verification**
```typescript
// GOOD - short-lived tokens with jose (works in Node.js and edge runtimes)
import { SignJWT, jwtVerify } from "jose";
const privateKey = new TextEncoder().encode(process.env.JWT_SECRET!);
async function createAccessToken(userId: string, role: string): Promise<string> {
return new SignJWT({ role, type: "access" })
.setProtectedHeader({ alg: "HS256" })
.setSubject(userId)
.setIssuedAt()
.setExpirationTime("15m")
.sign(privateKey);
}
async function createRefreshToken(userId: string): Promise<string> {
return new SignJWT({ type: "refresh", jti: crypto.randomUUID() })
.setProtectedHeader({ alg: "HS256" })
.setSubject(userId)
.setIssuedAt()
.setExpirationTime("7d")
.sign(privateKey);
}
async function verifyToken(token: string): Promise<{ sub: string; role?: string }> {
const { payload } = await jwtVerify(token, privateKey, {
algorithms: ["HS256"],
requiredClaims: ["sub", "exp", "type"],
});
return payload as { sub: string; role?: string };
}
```
**Secure cookie delivery**
```python
# GOOD - deliver tokens in httpOnly cookies, not in response body
from fastapi import Response
def set_auth_cookies(response: Response, access_token: str, refresh_token: str):
response.set_cookie(
key="access_token",
value=access_token,
httponly=True, # not accessible via JavaScript
secure=True, # HTTPS only
samesite="lax", # CSRF protection
max_age=int(ACCESS_TOKEN_EXPIRY.total_seconds()),
path="/",
)
response.set_cookie(
key="refresh_token",
value=refresh_token,
httponly=True,
secure=True,
samesite="lax",
max_age=int(REFRESH_TOKEN_EXPIRY.total_seconds()),
path="/auth/refresh", # only sent to refresh endpoint
)
def clear_auth_cookies(response: Response):
response.delete_cookie("access_token", path="/")
response.delete_cookie("refresh_token", path="/auth/refresh")
```
### 2. OAuth2 Flows
**Authorization code flow with PKCE (for SPAs and mobile apps)**
```typescript
// GOOD - PKCE flow for single-page applications (no client secret exposed)
import crypto from "crypto";
// Step 1: Generate code verifier and challenge
function generatePKCE(): { verifier: string; challenge: string } {
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto
.createHash("sha256")
.update(verifier)
.digest("base64url");
return { verifier, challenge };
}
// Step 2: Redirect user to authorization server
function getAuthorizationUrl(codeChallenge: string): string {
const params = new URLSearchParams({
response_type: "code",
client_id: process.env.OAUTH_CLIENT_ID!,
redirect_uri: process.env.OAUTH_REDIRECT_URI!,
scope: "openid profile email",
code_challenge: codeChallenge,
code_challenge_method: "S256",
state: crypto.randomBytes(16).toString("hex"),
});
return `https://auth.example.com/authorize?${params}`;
}
// Step 3: Exchange code for tokens on the callback
async function exchangeCode(code: string, codeVerifier: string): Promise<TokenSet> {
const response = await fetch("https://auth.example.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: process.env.OAUTH_REDIRECT_URI!,
client_id: process.env.OAUTH_CLIENT_ID!,
code_verifier: codeVerifier,
}),
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.status}`);
}
return response.json() as Promise<TokenSet>;
}
```
**Client credentials flow (server-to-server)**
```python
# GOOD - machine-to-machine auth with client credentials
import httpx
import os
async def get_service_token() -> str:
async with httpx.AsyncClient() as client:
response = await client.post(
"https://auth.example.com/token",
data={
"grant_type": "client_credentials",
"client_id": os.environ["SERVICE_CLIENT_ID"],
"client_secret": os.environ["SERVICE_CLIENT_SECRET"],
"scope": "read:data write:data",
},
)
response.raise_for_status()
return response.json()["access_token"]
```
**Python -- OAuth2 callback handling with FastAPI**
```python
# GOOD - secure callback handler with state validation
from fastapi import APIRouter, Request, HTTPException
from fastapi.responses import RedirectResponse
import httpx
router = APIRouter(prefix="/auth")
@router.get("/callback")
async def oauth_callback(request: Request, code: str, state: str):
# Validate state to prevent CSRF
stored_state = request.session.get("oauth_state")
if not stored_state or state != stored_state:
raise HTTPException(status_code=400, detail="Invalid state parameter")
code_verifier = request.session.pop("code_verifier")
async with httpx.AsyncClient() as client:
token_response = await client.post(
"https://auth.example.com/token",
data={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": os.environ["OAUTH_REDIRECT_URI"],
"client_id": os.environ["OAUTH_CLIENT_ID"],
"code_verifier": code_verifier,
},
)
token_response.raise_for_status()
tokens = token_response.json()
# Create local session from OAuth tokens
user_info = await fetch_user_info(tokens["access_token"])
user = await get_or_create_user(user_info)
response = RedirectResponse(url="/dashboard")
set_auth_cookies(response, create_access_token(user.id, user.role), create_refresh_token(user.id))
return response
```
### 3. Password Security
**Python -- argon2 (preferred) and bcrypt**
```python
# BAD - MD5 or SHA-256 alone is trivially crackable
import hashlib
hashed = hashlib.sha256(password.encode()).hexdigest()
# GOOD - argon2id (recommended by OWASP)
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(
time_cost=3, # iterations
memory_cost=65536, # 64 MB
parallelism=4,
)
def hash_password(password: str) -> str:
return ph.hash(password)
def verify_password(password: str, hashed: str) -> bool:
try:
return ph.verify(hashed, password)
except VerifyMismatchError:
return False
# GOOD - bcrypt alternative
from passlib.hash import bcrypt
hashed = bcrypt.using(rounds=12).hash(password)
is_valid = bcrypt.verify(password, hashed)
```
**TypeScript -- bcrypt**
```typescript
// GOOD - bcrypt with sufficient cost factor
import bcrypt from "bcrypt";
const SALT_ROUNDS = 12; // ~250ms on modern hardware, adjust as needed
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
```
**Password validation rules**
```typescript
// GOOD - enforce minimum complexity without overly restrictive rules
import { z } from "zod";
const PasswordSchema = z
.string()
.min(8, "Password must be at least 8 characters")
.max(128, "Password must not exceed 128 characters")
.regex(/[a-z]/, "Must contain at least one lowercase letter")
.regex(/[A-Z]/, "Must contain at least one uppercase letter")
.regex(/[0-9]/, "Must contain at least one digit");
// Python equivalent with Pydantic
from pydantic import BaseModel, field_validator
import re
class PasswordInput(BaseModel):
password: str
@field_validator("password")
@classmethod
def validate_password(cls, v: str) -> str:
if len(v) < 8:
raise ValueError("Password must be at least 8 characters")
if len(v) > 128:
raise ValueError("Password must not exceed 128 characters")
if not re.search(r"[a-z]", v):
raise ValueError("Must contain at least one lowercase letter")
if not re.search(r"[A-Z]", v):
raise ValueError("Must contain at least one uppercase letter")
if not re.search(r"[0-9]", v):
raise ValueError("Must contain at least one digit")
return v
```
**Timing-safe comparison**
```python
# BAD - standard equality leaks timing information
if stored_token == provided_token:
grant_access()
# GOOD - constant-time comparison prevents timing attacks
import hmac
def safe_compare(a: str, b: str) -> bool:
return hmac.compare_digest(a.encode(), b.encode())
```
```typescript
// GOOD - timing-safe comparison in Node.js
import crypto from "crypto";
function safeCompare(a: string, b: string): boolean {
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
}
```
### 4. Session Management
**Cookie-based sessions with Redis store (Express)**
```typescript
// GOOD - server-side sessions stored in Redis
import session from "express-session";
import RedisStore from "connect-redis";
import { createClient } from "redis";
const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();
app.use(
session({
store: new RedisStore({ client: redisClient, prefix: "sess:" }),
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 30 * 60 * 1000, // 30 minutes
},
name: "sid", // custom name -- do not use default "connect.sid"
}),
);
```
**Session fixation prevention**
```typescript
// GOOD - regenerate session ID after login to prevent fixation
app.post("/login", async (req, res) => {
const user = await authenticateUser(req.body.email, req.body.password);
if (!user) return res.status(401).json({ error: "Invalid credentials" });
// Regenerate session to prevent fixation attacks
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: "Session error" });
req.session.userId = user.id;
req.session.role = user.role;
req.session.loginAt = Date.now();
res.json({ user: { id: user.id, name: user.name } });
});
});
// GOOD - clear session fully on logout
app.post("/logout", (req, res) => {
req.session.destroy((err) => {
if (err) return res.status(500).json({ error: "Logout failed" });
res.clearCookie("sid");
res.json({ message: "Logged out" });
});
});
```
**Python -- FastAPI session with Redis**
```python
# GOOD - server-side session using Redis
from fastapi import Request, Response
import redis.asyncio as redis
import uuid
import json
redis_client = redis.from_url(os.environ["REDIS_URL"])
SESSION_TTL = 1800 # 30 minutes
async def create_session(response: Response, data: dict) -> str:
session_id = str(uuid.uuid4())
await redis_client.setex(f"session:{session_id}", SESSION_TTL, json.dumps(data))
response.set_cookie(
key="session_id",
value=session_id,
httponly=True,
secure=True,
samesite="lax",
max_age=SESSION_TTL,
)
return session_id
async def get_session(request: Request) -> dict | None:
session_id = request.cookies.get("session_id")
if not session_id:
return None
data = await redis_client.get(f"session:{session_id}")
if data:
# Refresh TTL on access (sliding expiry)
await redis_client.expire(f"session:{session_id}", SESSION_TTL)
return json.loads(data)
return None
async def destroy_session(request: Request, response: Response):
session_id = request.cookies.get("session_id")
if session_id:
await redis_client.delete(f"session:{session_id}")
response.delete_cookie("session_id")
```
### 5. RBAC Patterns
**Role and permission model**
```python
# GOOD - permission-based RBAC, not just role names
from enum import Enum
class Permission(str, Enum):
READ_POSTS = "read:posts"
WRITE_POSTS = "write:posts"
DELETE_POSTS = "delete:posts"
MANAGE_USERS = "manage:users"
ADMIN_ALL = "admin:all"
ROLE_PERMISSIONS: dict[str, set[Permission]] = {
"viewer": {Permission.READ_POSTS},
"editor": {Permission.READ_POSTS, Permission.WRITE_POSTS},
"admin": {Permission.READ_POSTS, Permission.WRITE_POSTS, Permission.DELETE_POSTS, Permission.MANAGE_USERS},
"superadmin": {Permission.ADMIN_ALL},
}
def has_permission(user_role: str, required: Permission) -> bool:
permissions = ROLE_PERMISSIONS.get(user_role, set())
return required in permissions or Permission.ADMIN_ALL in permissions
```
**FastAPI -- dependency-based authorization**
```python
# GOOD - reusable auth dependency with permission check
from fastapi import Depends, HTTPException, Request
async def get_current_user(request: Request) -> User:
token = request.cookies.get("access_token")
if not token:
raise HTTPException(status_code=401, detail="Not authenticated")
try:
payload = decode_token(token)
user = await user_repo.get(int(payload["sub"]))
if not user:
raise HTTPException(status_code=401, detail="User not found")
return user
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
def require_permission(permission: Permission):
async def checker(user: User = Depends(get_current_user)):
if not has_permission(user.role, permission):
raise HTTPException(status_code=403, detail="Insufficient permissions")
return user
return checker
@app.delete("/posts/{post_id}")
async def delete_post(
post_id: int,
user: User = Depends(require_permission(Permission.DELETE_POSTS)),
):
post = await post_repo.get(post_id)
if not post:
raise HTTPException(status_code=404)
await post_repo.delete(post_id)
return {"deleted": True}
```
**Express -- middleware-based authorization**
```typescript
// GOOD - composable permission middleware
interface AuthUser {
id: string;
role: string;
permissions: string[];
}
function requirePermission(...required: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
const user = req.user as AuthUser | undefined;
if (!user) {
return res.status(401).json({ error: "Not authenticated" });
}
const hasAll = required.every(
(perm) => user.permissions.includes(perm) || user.permissions.includes("admin:all"),
);
if (!hasAll) {
return res.status(403).json({ error: "Insufficient permissions" });
}
next();
};
}
// Usage
app.delete("/posts/:id", requirePermission("delete:posts"), deletePostHandler);
app.get("/admin/users", requirePermission("manage:users"), listUsersHandler);
```
### 6. Protected Routes
**Next.js middleware (App Router)**
```typescript
// middleware.ts -- runs on every matching request at the edge
import { NextRequest, NextResponse } from "next/server";
import { jwtVerify } from "jose";
const PUBLIC_PATHS = ["/", "/login", "/signup", "/api/auth"];
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Allow public paths
if (PUBLIC_PATHS.some((p) => pathname.startsWith(p))) {
return NextResponse.next();
}
const token = request.cookies.get("access_token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
try {
const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
await jwtVerify(token, secret);
return NextResponse.next();
} catch {
// Token invalid or expired -- redirect to login
const response = NextResponse.redirect(new URL("/login", request.url));
response.cookies.delete("access_token");
return response;
}
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
```
**FastAPI -- dependency injection guard**
```python
# GOOD - protect entire router with a dependency
from fastapi import APIRouter, Depends
protected_router = APIRouter(
prefix="/api/v1",
dependencies=[Depends(get_current_user)], # all routes require auth
)
@protected_router.get("/profile")
async def get_profile(user: User = Depends(get_current_user)):
return {"id": user.id, "name": user.name, "role": user.role}
@protected_router.get("/admin/stats")
async def admin_stats(user: User = Depends(require_permission(Permission.ADMIN_ALL))):
return await compute_stats()
# Mount protected and public routers separately
app.include_router(auth_router) # /auth/* -- public
app.include_router(protected_router) # /api/v1/* -- requires auth
```
**Express -- route-level guard**
```typescript
// GOOD - auth middleware applied selectively
function requireAuth(req: Request, res: Response, next: NextFunction) {
const token = req.cookies.access_token;
if (!token) {
return res.status(401).json({ error: "Authentication required" });
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!);
req.user = payload as AuthUser;
next();
} catch {
res.clearCookie("access_token");
return res.status(401).json({ error: "Invalid or expired token" });
}
}
// Public routes
app.post("/auth/login", loginHandler);
app.post("/auth/register", registerHandler);
// Protected routes
app.use("/api", requireAuth);
app.get("/api/profile", profileHandler);
app.get("/api/posts", listPostsHandler);
```
### 7. Multi-Factor Authentication (TOTP)
**Python -- pyotp**
```python
# GOOD - TOTP setup and verification
import pyotp
def generate_totp_secret() -> str:
"""Generate a new TOTP secret for a user."""
return pyotp.random_base32()
def get_totp_provisioning_uri(secret: str, user_email: str, issuer: str = "MyApp") -> str:
"""Generate a QR code URI for authenticator app setup."""
return pyotp.totp.TOTP(secret).provisioning_uri(
name=user_email,
issuer_name=issuer,
)
def verify_totp(secret: str, code: str) -> bool:
"""Verify a TOTP code with a 30-second window tolerance."""
totp = pyotp.TOTP(secret)
return totp.verify(code, valid_window=1) # allows +/- 30 seconds
```
**TypeScript -- otplib**
```typescript
// GOOD - TOTP with otplib
import { authenticator } from "otplib";
function generateTotpSecret(): string {
return authenticator.generateSecret();
}
function getTotpUri(secret: string, email: string): string {
return authenticator.keyuri(email, "MyApp", secret);
}
function verifyTotp(secret: string, code: string): boolean {
return authenticator.check(code, secret);
}
```
**Backup codes**
```python
# GOOD - generate one-time backup codes for MFA recovery
import secrets
def generate_backup_codes(count: int = 10) -> list[str]:
"""Generate single-use backup codes. Store hashed, show once."""
return [secrets.token_hex(4).upper() for _ in range(count)]
# Example output: ["A1B2C3D4", "E5F6A7B8", ...]
# Store hashed backup codes in the database
from argon2 import PasswordHasher
ph = PasswordHasher()
async def store_backup_codes(user_id: int, codes: list[str]):
hashed_codes = [ph.hash(code) for code in codes]
await db.execute(
"UPDATE users SET backup_codes = $1 WHERE id = $2",
[json.dumps(hashed_codes), user_id],
)
async def verify_backup_code(user_id: int, code: str) -> bool:
user = await db.get(User, user_id)
hashed_codes = json.loads(user.backup_codes)
for i, hashed in enumerate(hashed_codes):
try:
if ph.verify(hashed, code):
# Remove used code (single-use)
hashed_codes.pop(i)
await db.execute(
"UPDATE users SET backup_codes = $1 WHERE id = $2",
[json.dumps(hashed_codes), user_id],
)
return True
except Exception:
continue
return False
```
**MFA login flow**
```python
# GOOD - two-step login: credentials first, then MFA
@router.post("/auth/login")
async def login(credentials: LoginRequest, response: Response):
user = await authenticate_user(credentials.email, credentials.password)
if not user:
raise HTTPException(status_code=401, detail="Invalid credentials")
if user.mfa_enabled:
# Issue a short-lived MFA challenge token (not a full session)
mfa_token = create_mfa_challenge_token(user.id)
return {"requires_mfa": True, "mfa_token": mfa_token}
# No MFA -- issue full tokens
set_auth_cookies(response, create_access_token(user.id, user.role), create_refresh_token(user.id))
return {"user": {"id": user.id, "name": user.name}}
@router.post("/auth/mfa/verify")
async def verify_mfa(payload: MfaVerifyRequest, response: Response):
# Validate the MFA challenge token
challenge = decode_mfa_challenge_token(payload.mfa_token)
user = await user_repo.get(challenge["user_id"])
if not verify_totp(user.totp_secret, payload.code):
# Also check backup codes as fallback
if not await verify_backup_code(user.id, payload.code):
raise HTTPException(status_code=401, detail="Invalid MFA code")
set_auth_cookies(response, create_access_token(user.id, user.role), create_refresh_token(user.id))
return {"user": {"id": user.id, "name": user.name}}
```
---
## Best Practices
1. **Use short-lived access tokens (5-15 minutes) paired with refresh tokens (7-30 days).** Short access tokens limit the damage window if a token is compromised. Refresh tokens allow seamless re-authentication without re-entering credentials.
2. **Deliver tokens in httpOnly, secure, sameSite cookies.** Never return tokens in JSON response bodies for browser-based apps. httpOnly prevents XSS from reading the token, secure ensures HTTPS-only transmission, and sameSite=lax mitigates CSRF.
3. **Hash passwords with argon2id or bcrypt, never with MD5, SHA-1, or SHA-256 alone.** Adaptive hashing functions include a work factor that makes brute-force attacks computationally expensive. Increase the cost factor as hardware improves.
4. **Regenerate session IDs after login.** Session fixation attacks exploit predictable or reused session IDs. Always issue a new session ID after successful authentication.
5. **Validate the state parameter in OAuth2 callbacks.** The state parameter prevents CSRF attacks during the authorization flow. Generate a cryptographically random value, store it in the session, and verify it when the callback arrives.
6. **Implement token revocation for refresh tokens.** Store refresh token JTIs (unique identifiers) in a database or Redis. On logout, revoke all active refresh tokens for the user. Check the revocation list on every refresh attempt.
7. **Apply the principle of least privilege in RBAC.** Default new users to the most restrictive role. Grant permissions explicitly, not implicitly. Check permissions at the object level, not just the role level.
8. **Rate-limit authentication endpoints aggressively.** Apply strict rate limits (5-10 attempts per minute) on login, registration, password reset, and MFA verification endpoints. Use both IP-based and account-based limiting.
---
## Common Pitfalls
1. **Storing JWTs in localStorage.** Any XSS vulnerability can steal the token. Use httpOnly cookies instead. If you must use localStorage (e.g., for native apps), pair it with strict CSP and regular XSS auditing.
2. **Not validating the token type claim.** Without a `type` field in the JWT payload, a refresh token could be used as an access token and vice versa. Always include and verify a `type` claim.
3. **Using symmetric keys (HS256) with shared secrets across services.** If multiple services verify tokens, any service that can verify can also forge tokens. Use asymmetric keys (RS256/ES256) so only the auth service holds the private key.
4. **Checking authentication but not authorization.** A valid token proves identity but not permission. Always verify that the authenticated user has the specific permission required for the requested action.
5. **Returning different error messages for "user not found" vs "wrong password."** This leaks information about which accounts exist (user enumeration). Return a generic "Invalid credentials" message for both cases.
6. **Not setting absolute session timeouts.** Sliding expiry alone means a session can live forever with continuous activity. Set an absolute maximum lifetime (e.g., 8 hours) in addition to idle timeout (e.g., 30 minutes).
---
## Security Checklist
- [ ] Access tokens expire within 15 minutes, refresh tokens within 7-30 days
- [ ] Tokens delivered in httpOnly, secure, sameSite cookies (not localStorage)
- [ ] Passwords hashed with argon2id or bcrypt (12+ rounds)
- [ ] Session IDs regenerated after successful login
- [ ] OAuth2 state parameter validated on callback
- [ ] Refresh tokens have unique JTI and can be revoked
- [ ] RBAC permissions checked at object level, not just role level
- [ ] Login, registration, and password reset endpoints are rate-limited
- [ ] Error messages do not distinguish between "user not found" and "wrong password"
- [ ] MFA backup codes are hashed and single-use
- [ ] TOTP secrets are stored encrypted at rest
- [ ] Absolute session timeout enforced alongside sliding expiry
- [ ] CSRF protection in place for all state-changing endpoints
---
## 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
@@ -0,0 +1,237 @@
# Authentication Flows Quick Reference
## Decision Tree: Which Auth Method?
```
What are you building?
├─ Server-rendered web app (Next.js, Django, Rails)?
│ └─> SESSION-BASED AUTH
│ - HttpOnly cookies, server-side session store
│ - Simple, secure, well-understood
├─ SPA + API backend (same domain)?
│ └─> SESSION-BASED AUTH (still preferred)
│ - Cookies sent automatically, no JS token handling
│ - Or: JWT in HttpOnly cookie (not localStorage)
├─ SPA + API backend (different domain)?
│ └─> JWT with access + refresh tokens
│ - Access token: short-lived, in memory
│ - Refresh token: HttpOnly cookie
├─ Mobile app?
│ └─> JWT with access + refresh tokens
│ - Store refresh token in secure storage (Keychain/Keystore)
│ - Access token in memory
├─ Third-party API access?
│ └─> OAuth2 + API keys
│ - OAuth2 for user-delegated access
│ - API keys for server-to-server
├─ Machine-to-machine (service-to-service)?
│ └─> API KEYS or OAuth2 Client Credentials
│ - API key: simple, rotate regularly
│ - Client Credentials: when you need scoped access
└─ CLI tool?
└─> OAuth2 Device Code Flow or API key
```
---
## JWT Access + Refresh Token Flow
```
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Client │ │ Auth │ │ API │
│ (SPA/ │ │ Server │ │ Server │
│ Mobile) │ │ │ │ │
└────┬──────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ 1. POST /auth/login │ │
│ { email, password } │ │
│─────────────────────────────>│ │
│ │ │
│ 2. 200 OK │ │
│ { access_token (15min) } │ │
│ Set-Cookie: refresh_token │ │
│ (HttpOnly, Secure, 7d) │ │
│<─────────────────────────────│ │
│ │ │
│ 3. GET /api/data │ │
│ Authorization: Bearer <access_token> │
│─────────────────────────────────────────────────────────────>│
│ │ │
│ 4. 200 OK { data } │ │
│<─────────────────────────────────────────────────────────────│
│ │ │
│ ── access_token expires ── │ │
│ │ │
│ 5. GET /api/data │ │
│ Authorization: Bearer <expired_token> │
│─────────────────────────────────────────────────────────────>│
│ │ │
│ 6. 401 { code: "token_expired" } │
│<─────────────────────────────────────────────────────────────│
│ │ │
│ 7. POST /auth/refresh │ │
│ Cookie: refresh_token │ │
│─────────────────────────────>│ │
│ │ │
│ 8. 200 { new access_token } │ │
│ Set-Cookie: new refresh │ │
│<─────────────────────────────│ │
│ │ │
│ 9. Retry original request with new access_token │
│─────────────────────────────────────────────────────────────>│
```
### JWT Best Practices
| Concern | Recommendation |
|---------|---------------|
| Access token lifetime | 5-15 minutes |
| Refresh token lifetime | 7-30 days |
| Access token storage | Memory only (JS variable) |
| Refresh token storage | HttpOnly Secure cookie (web) or Keychain (mobile) |
| Token rotation | Issue new refresh token on each refresh |
| Revocation | Maintain server-side deny list for refresh tokens |
| Algorithm | RS256 (asymmetric) for distributed systems, HS256 for single server |
| Claims | Minimal: sub, exp, iat, roles/permissions |
---
## OAuth2 Authorization Code + PKCE Flow
```
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Client │ │ Auth │ │ Resource │
│ (Browser)│ │ Provider│ │ Server │
└────┬──────┘ └────┬─────┘ └────┬──────┘
│ │ │
│ 1. Generate: │ │
│ code_verifier (random 43-128 chars) │
│ code_challenge = SHA256(code_verifier) │
│ │ │
│ 2. Redirect to: │ │
│ /authorize? │ │
│ response_type=code │
│ client_id=xxx │ │
│ redirect_uri=https://app/callback │
│ scope=openid profile │
│ state=random_csrf_token │
│ code_challenge=xxx │
│ code_challenge_method=S256 │
│────────────────────>│ │
│ │ │
│ 3. User logs in │ │
│ and consents │ │
│ │ │
│ 4. Redirect to: │ │
│ /callback?code=AUTH_CODE&state=xxx │
│<────────────────────│ │
│ │ │
│ 5. Verify state matches │
│ │ │
│ 6. POST /token │ │
│ { grant_type=authorization_code, │
│ code=AUTH_CODE, │ │
│ redirect_uri=...,│ │
│ code_verifier=ORIGINAL_VERIFIER } │
│────────────────────>│ │
│ │ │
│ 7. { access_token, │ │
│ refresh_token,│ │
│ id_token } │ │
│<────────────────────│ │
│ │ │
│ 8. GET /api/resource │
│ Authorization: Bearer <access_token> │
│───────────────────────────────────────────>│
```
### PKCE Key Points
| Term | Purpose |
|------|---------|
| `code_verifier` | Random string (43-128 chars), stored client-side |
| `code_challenge` | `BASE64URL(SHA256(code_verifier))` sent in auth request |
| `state` | CSRF protection (random, verify on callback) |
| PKCE purpose | Prevents auth code interception (no client secret needed) |
---
## Session-Based Auth Flow
```
┌──────────┐ ┌──────────────────┐
│ Browser │ │ Server │
│ │ │ (session store) │
└────┬──────┘ └────┬──────────────┘
│ │
│ 1. POST /login │
│ { email, password } │
│─────────────────────────────>│
│ │ 2. Verify credentials
│ │ 3. Create session in store
│ │ (Redis/DB/memory)
│ 4. Set-Cookie: │
│ session_id=abc123; │
│ HttpOnly; Secure; │
│ SameSite=Lax; Path=/ │
│<─────────────────────────────│
│ │
│ 5. GET /dashboard │
│ Cookie: session_id=abc123 │ (sent automatically)
│─────────────────────────────>│
│ │ 6. Lookup session abc123
│ │ 7. Attach user to request
│ 8. 200 OK │
│<─────────────────────────────│
│ │
│ 9. POST /logout │
│─────────────────────────────>│
│ │ 10. Delete session from store
│ 11. Clear cookie │
│<─────────────────────────────│
```
### Session Cookie Settings
| Attribute | Value | Purpose |
|-----------|-------|---------|
| `HttpOnly` | Always | Prevent JS access (XSS protection) |
| `Secure` | Always in prod | Only send over HTTPS |
| `SameSite` | `Lax` (default) | CSRF protection (allows top-level navigation) |
| `SameSite` | `Strict` | Stronger CSRF (breaks external link login) |
| `Path` | `/` | Cookie scope |
| `Max-Age` | 86400-2592000 | Session duration (1-30 days) |
| `Domain` | Omit or explicit | Cookie scope to domain |
---
## Comparison: JWT vs Sessions vs API Keys
| Aspect | JWT | Sessions | API Keys |
|--------|-----|----------|----------|
| Stateless | Yes (no server lookup) | No (server-side store) | No (server-side lookup) |
| Revocation | Hard (needs deny list) | Easy (delete session) | Easy (delete key) |
| Scaling | Easy (no shared state) | Needs shared session store | Needs shared key store |
| Security | Token theft = access until expiry | Session theft = access until revoked | Key theft = access until rotated |
| Best for | Distributed APIs, mobile | Web apps, SSR | Service-to-service, CLI |
| CSRF risk | Low (if not in cookie) | Needs CSRF tokens | N/A (header-based) |
| XSS risk | High if in localStorage | Low (HttpOnly cookie) | Low (server-side only) |
### API Key Best Practices
| Practice | Details |
|----------|---------|
| Prefix keys | `sk_live_`, `pk_test_` (identify type/env) |
| Hash before storing | Store `SHA256(key)`, never plaintext |
| Scope keys | Limit permissions per key |
| Set expiry | Auto-expire, require rotation |
| Rate limit per key | Prevent abuse |
| Transmit in header | `Authorization: Bearer <key>` or `X-API-Key: <key>` |
| Never in URL | Query params end up in logs and browser history |
+786
View File
@@ -0,0 +1,786 @@
---
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
## When to Use
- Adding memoization to expensive pure functions or computations
- Setting HTTP cache headers on API responses or static assets
- Implementing a Redis or in-memory cache layer for database queries
- Configuring CDN caching rules for edge distribution
- Designing cache invalidation strategies for data that changes
- Optimizing Next.js data fetching with built-in caching primitives
## When NOT to Use
- Data that must always be real-time and consistent (financial transactions, inventory counts during checkout) — caching introduces staleness
- Write-heavy workloads where invalidation cost exceeds the read savings
- Small datasets that are fast to compute or fetch — the overhead of cache management is not worth it
---
## Core Patterns
### 1. Memoization
Memoization caches the result of a function call based on its arguments. Use it for pure functions (same input always produces same output) that are called repeatedly with the same arguments.
#### Python — functools
```python
from functools import lru_cache, cache
# lru_cache with a max size — evicts least recently used entries
@lru_cache(maxsize=256)
def compute_shipping_cost(weight_kg: float, zone: str) -> float:
"""Expensive calculation based on weight and shipping zone."""
# Complex rate lookup, distance calculation, surcharges...
return base_rate * weight_factor * zone_multiplier
# cache (Python 3.9+) — unbounded, equivalent to lru_cache(maxsize=None)
@cache
def parse_config(config_path: str) -> dict:
"""Parse a config file. Result never changes for the same path."""
with open(config_path) as f:
return yaml.safe_load(f)
# Check cache statistics
print(compute_shipping_cost.cache_info())
# CacheInfo(hits=142, misses=23, maxsize=256, currsize=23)
# Clear cache when needed
compute_shipping_cost.cache_clear()
```
**Important:** `lru_cache` requires hashable arguments. It does not work with lists, dicts, or mutable objects. For async functions, use `asyncache` or `aiocache`:
```python
# Async memoization with aiocache
from aiocache import cached, Cache
@cached(
ttl=300, # 5 minutes
cache=Cache.MEMORY,
key_builder=lambda f, *args, **kwargs: f"user_profile:{args[0]}",
)
async def get_user_profile(user_id: int) -> UserProfile:
return await user_repo.get_with_preferences(user_id)
```
#### React — useMemo and useCallback
```tsx
import { useMemo, useCallback } from "react";
interface OrderSummaryProps {
items: OrderItem[];
taxRate: number;
onCheckout: (total: number) => void;
}
function OrderSummary({ items, taxRate, onCheckout }: OrderSummaryProps) {
// Memoize expensive computation — recalculates only when items or taxRate change
const totals = useMemo(() => {
const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
const tax = subtotal * taxRate;
const total = subtotal + tax;
return { subtotal, tax, total };
}, [items, taxRate]);
// Memoize callback to avoid re-renders in child components
const handleCheckout = useCallback(() => {
onCheckout(totals.total);
}, [onCheckout, totals.total]);
return (
<div>
<p>Subtotal: ${totals.subtotal.toFixed(2)}</p>
<p>Tax: ${totals.tax.toFixed(2)}</p>
<p>Total: ${totals.total.toFixed(2)}</p>
<CheckoutButton onClick={handleCheckout} />
</div>
);
}
```
**When NOT to memoize in React:** Do not wrap every value in `useMemo`. If the computation is trivial (simple arithmetic, string concatenation, array access), the overhead of memoization exceeds the cost of recalculation. Memoize only when profiling shows a performance problem or when the value is passed as a prop to a `React.memo` child.
### 2. HTTP Caching
HTTP caching lets browsers and CDNs serve responses without hitting your server. Get this right and you can eliminate 80% or more of redundant requests.
#### Cache-Control headers
```python
# Python — FastAPI response headers
from fastapi import Response
@router.get("/api/v1/products/{product_id}")
async def get_product(product_id: int, response: Response):
product = await product_service.get(product_id)
# Public: CDN and browser can cache. max-age: browser TTL. s-maxage: CDN TTL.
response.headers["Cache-Control"] = "public, max-age=60, s-maxage=300"
return product
@router.get("/api/v1/me/profile")
async def get_my_profile(response: Response, user: User = Depends(get_current_user)):
# Private: only browser cache, not CDN (contains user-specific data)
response.headers["Cache-Control"] = "private, max-age=300"
return user
@router.post("/api/v1/orders")
async def create_order(order: OrderCreate, response: Response):
# No cache for mutations
response.headers["Cache-Control"] = "no-store"
return await order_service.create(order)
```
```typescript
// TypeScript — Express response headers
app.get("/api/v1/products/:id", async (req, res) => {
const product = await productService.get(req.params.id);
res.set("Cache-Control", "public, max-age=60, s-maxage=300");
res.json(product);
});
// stale-while-revalidate: serve stale content while fetching fresh in background
app.get("/api/v1/feed", async (req, res) => {
const feed = await feedService.getLatest();
// Browser uses cache for 60s, then serves stale for up to 600s while revalidating
res.set(
"Cache-Control",
"public, max-age=60, s-maxage=300, stale-while-revalidate=600"
);
res.json(feed);
});
```
#### ETag and conditional requests
```python
# Python — ETag-based caching
import hashlib
from fastapi import Request, Response
@router.get("/api/v1/catalog")
async def get_catalog(request: Request, response: Response):
catalog = await catalog_service.get_full()
catalog_json = json.dumps(catalog, sort_keys=True)
# Generate ETag from content hash
etag = f'"{hashlib.md5(catalog_json.encode()).hexdigest()}"'
# Check if client already has this version
if_none_match = request.headers.get("If-None-Match")
if if_none_match == etag:
return Response(status_code=304) # Not Modified
response.headers["ETag"] = etag
response.headers["Cache-Control"] = "public, max-age=0, must-revalidate"
return catalog
```
```typescript
// TypeScript — ETag middleware
import { createHash } from "node:crypto";
app.get("/api/v1/catalog", async (req, res) => {
const catalog = await catalogService.getFull();
const body = JSON.stringify(catalog);
const etag = `"${createHash("md5").update(body).digest("hex")}"`;
if (req.headers["if-none-match"] === etag) {
res.status(304).end();
return;
}
res.set("ETag", etag);
res.set("Cache-Control", "public, max-age=0, must-revalidate");
res.json(catalog);
});
```
#### Cache-Control cheat sheet
| Directive | Meaning |
|-----------|---------|
| `public` | Any cache (CDN, browser) may store the response |
| `private` | Only the browser may cache (not CDN) |
| `no-store` | Do not cache at all |
| `no-cache` | Cache but revalidate with server before using |
| `max-age=N` | Browser cache TTL in seconds |
| `s-maxage=N` | CDN/proxy cache TTL in seconds (overrides max-age for shared caches) |
| `must-revalidate` | Once stale, must revalidate before using |
| `stale-while-revalidate=N` | Serve stale while fetching fresh in background for N seconds |
| `immutable` | Content will never change (use for hashed assets like `app.a1b2c3.js`) |
### 3. Redis Caching
Redis is the standard external cache for web applications. It survives process restarts, can be shared across multiple servers, and supports TTL-based expiry natively.
#### Cache-aside pattern (read-through)
The most common pattern: check cache first, fetch from source on miss, populate cache for next time.
```python
# Python — redis cache-aside
import json
from typing import TypeVar, Callable, Awaitable
import redis.asyncio as redis
T = TypeVar("T")
class RedisCache:
def __init__(self, redis_url: str, default_ttl: int = 300):
self.client = redis.from_url(redis_url, decode_responses=True)
self.default_ttl = default_ttl
async def get_or_set(
self,
key: str,
fetch_fn: Callable[[], Awaitable[T]],
ttl: int | None = None,
) -> T:
"""Cache-aside: return cached value or fetch, cache, and return."""
cached = await self.client.get(key)
if cached is not None:
return json.loads(cached)
# Cache miss — fetch from source
value = await fetch_fn()
await self.client.set(
key,
json.dumps(value, default=str),
ex=ttl or self.default_ttl,
)
return value
async def invalidate(self, key: str) -> None:
await self.client.delete(key)
async def invalidate_pattern(self, pattern: str) -> None:
"""Delete all keys matching a pattern (e.g., 'user:42:*')."""
cursor = 0
while True:
cursor, keys = await self.client.scan(cursor, match=pattern, count=100)
if keys:
await self.client.delete(*keys)
if cursor == 0:
break
# Usage
cache = RedisCache("redis://localhost:6379/0")
async def get_user_profile(user_id: int) -> UserProfile:
return await cache.get_or_set(
key=f"user_profile:{user_id}",
fetch_fn=lambda: user_repo.get_with_preferences(user_id),
ttl=600, # 10 minutes
)
async def update_user_profile(user_id: int, data: UserUpdate) -> UserProfile:
profile = await user_repo.update(user_id, data)
await cache.invalidate(f"user_profile:{user_id}")
return profile
```
```typescript
// TypeScript — ioredis cache-aside
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
export class RedisCache {
constructor(private defaultTtl = 300) {}
async getOrSet<T>(
key: string,
fetchFn: () => Promise<T>,
ttl?: number
): Promise<T> {
const cached = await redis.get(key);
if (cached !== null) {
return JSON.parse(cached) as T;
}
const value = await fetchFn();
await redis.set(key, JSON.stringify(value), "EX", ttl ?? this.defaultTtl);
return value;
}
async invalidate(key: string): Promise<void> {
await redis.del(key);
}
async invalidatePattern(pattern: string): Promise<void> {
const stream = redis.scanStream({ match: pattern, count: 100 });
const pipeline = redis.pipeline();
for await (const keys of stream) {
for (const key of keys as string[]) {
pipeline.del(key);
}
}
await pipeline.exec();
}
}
```
#### Write-through pattern
Update cache and source simultaneously on writes. Guarantees cache is always fresh at the cost of slower writes.
```python
async def update_product(product_id: int, data: ProductUpdate) -> Product:
# Update database
product = await product_repo.update(product_id, data)
# Update cache atomically
await cache.client.set(
f"product:{product_id}",
json.dumps(product.model_dump(), default=str),
ex=3600,
)
return product
```
#### TTL strategies
| Data Type | Recommended TTL | Rationale |
|-----------|----------------|-----------|
| User session data | 15-30 minutes | Balance security with UX |
| Product catalog | 5-60 minutes | Changes infrequently, high read volume |
| Search results | 1-5 minutes | Acceptable staleness, expensive to compute |
| Configuration | 5-15 minutes | Rarely changes, critical path |
| Rate limit counters | Match the rate limit window | Must be precise |
| Feature flags | 30-60 seconds | Needs to propagate quickly |
### 4. Application Cache
For single-server applications or per-process caching where Redis is overkill.
#### Python — cachetools
```python
from cachetools import TTLCache, LRUCache
from cachetools.keys import hashkey
import asyncio
# TTL cache: entries expire after 300 seconds, max 1000 entries
user_cache: TTLCache = TTLCache(maxsize=1000, ttl=300)
# LRU cache: evicts least recently used when full
template_cache: LRUCache = LRUCache(maxsize=100)
# Thread-safe / async-safe wrapper
_cache_lock = asyncio.Lock()
async def get_user_cached(user_id: int) -> User:
key = hashkey(user_id)
async with _cache_lock:
if key in user_cache:
return user_cache[key]
# Fetch outside the lock to avoid holding it during I/O
user = await user_repo.get(user_id)
async with _cache_lock:
user_cache[key] = user
return user
```
#### TypeScript — node-cache
```typescript
import NodeCache from "node-cache";
// stdTTL: default TTL in seconds, checkperiod: cleanup interval
const cache = new NodeCache({ stdTTL: 300, checkperiod: 60 });
export async function getUserCached(userId: number): Promise<User> {
const cacheKey = `user:${userId}`;
const cached = cache.get<User>(cacheKey);
if (cached !== undefined) {
return cached;
}
const user = await userRepo.findById(userId);
cache.set(cacheKey, user);
return user;
}
// Listen for eviction events
cache.on("expired", (key: string, value: unknown) => {
log.debug({ key }, "cache_entry_expired");
});
// Cache statistics
const stats = cache.getStats();
// { hits: 1523, misses: 89, keys: 234, ksize: 4680, vsize: 156000 }
```
### 5. CDN Caching
CDN caching moves content to edge servers close to users. Configure it correctly and your origin server handles a fraction of the traffic.
#### Cloudflare cache rules
```python
# Python — set headers that Cloudflare respects
@router.get("/api/v1/public/articles")
async def list_articles(response: Response):
articles = await article_service.list_published()
# Cloudflare respects s-maxage for edge cache TTL
response.headers["Cache-Control"] = "public, s-maxage=3600, max-age=60"
# Vary tells the CDN to cache different versions for different values
response.headers["Vary"] = "Accept-Encoding, Accept-Language"
return articles
# Hashed static assets — cache forever
@router.get("/assets/{filename}")
async def serve_asset(filename: str, response: Response):
# Filenames contain content hash: app.a3b2c1.js
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
return FileResponse(f"static/{filename}")
```
#### Cache purge on content update
```python
import httpx
async def purge_cdn_cache(urls: list[str]) -> None:
"""Purge specific URLs from Cloudflare edge cache."""
async with httpx.AsyncClient() as client:
await client.post(
f"https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/purge_cache",
headers={"Authorization": f"Bearer {CF_API_TOKEN}"},
json={"files": urls},
)
```
```typescript
// TypeScript — purge after content update
export async function purgeCache(urls: string[]): Promise<void> {
await fetch(
`https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache`,
{
method: "POST",
headers: {
Authorization: `Bearer ${CF_API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ files: urls }),
}
);
}
// Purge after publishing an article
export async function publishArticle(id: string): Promise<void> {
await articleRepo.publish(id);
await purgeCache([
`https://example.com/api/v1/articles/${id}`,
`https://example.com/api/v1/articles`, // List endpoint
]);
}
```
#### Vary header
The `Vary` header tells the CDN to maintain separate cached versions for different request header values:
```
Vary: Accept-Encoding → separate cache for gzip vs brotli
Vary: Accept-Language → separate cache per language
Vary: Accept-Encoding, Authorization → DO NOT DO THIS — Authorization varies per user, so nothing is ever cached
```
**Rule:** Never include `Authorization`, `Cookie`, or other high-cardinality headers in `Vary` unless you specifically want per-user caching (which defeats the purpose of a CDN).
### 6. Cache Invalidation
The two hardest problems in computer science are cache invalidation, naming things, and off-by-one errors. Here are patterns that make invalidation manageable.
#### Tag-based invalidation
Group related cache entries under tags so you can invalidate them together.
```python
class TaggedCache:
"""Redis-backed cache with tag-based invalidation."""
def __init__(self, redis_client):
self.redis = redis_client
async def set(self, key: str, value: str, ttl: int, tags: list[str]) -> None:
pipe = self.redis.pipeline()
pipe.set(key, value, ex=ttl)
for tag in tags:
pipe.sadd(f"tag:{tag}", key)
pipe.expire(f"tag:{tag}", ttl + 60) # Tag lives slightly longer
await pipe.execute()
async def get(self, key: str) -> str | None:
return await self.redis.get(key)
async def invalidate_tag(self, tag: str) -> int:
"""Delete all cache entries associated with a tag."""
tag_key = f"tag:{tag}"
keys = await self.redis.smembers(tag_key)
if keys:
pipe = self.redis.pipeline()
pipe.delete(*keys)
pipe.delete(tag_key)
results = await pipe.execute()
return results[0] # Number of deleted keys
return 0
# Usage
cache = TaggedCache(redis_client)
# Cache a product, tagged with its category and brand
await cache.set(
key=f"product:{product.id}",
value=json.dumps(product.dict()),
ttl=3600,
tags=[f"category:{product.category_id}", f"brand:{product.brand_id}"],
)
# When a category is updated, invalidate all products in that category
await cache.invalidate_tag(f"category:{category_id}")
```
#### Event-driven invalidation
Invalidate caches in response to domain events rather than inline in business logic.
```python
# events.py
from dataclasses import dataclass
@dataclass
class ProductUpdated:
product_id: int
category_id: int
@dataclass
class CategoryUpdated:
category_id: int
# event_handlers.py
async def handle_product_updated(event: ProductUpdated) -> None:
await cache.invalidate(f"product:{event.product_id}")
await cache.invalidate(f"product_list:category:{event.category_id}")
async def handle_category_updated(event: CategoryUpdated) -> None:
await cache.invalidate_tag(f"category:{event.category_id}")
```
#### Versioned keys
Instead of deleting cache entries, change the key so old entries become unreachable and expire naturally.
```python
async def get_catalog_version() -> int:
"""Stored in Redis or database. Increment on catalog changes."""
version = await redis.get("catalog:version")
return int(version) if version else 1
async def get_catalog_cached() -> list[Product]:
version = await get_catalog_version()
key = f"catalog:v{version}"
return await cache.get_or_set(key, catalog_service.get_all, ttl=3600)
async def on_catalog_change() -> None:
"""Called when any product is added, updated, or removed."""
await redis.incr("catalog:version")
# Old version keys expire naturally via TTL — no explicit deletion needed
```
### 7. Next.js Caching
Next.js has multiple cache layers. Understanding which layer applies to your use case avoids common bugs.
#### Data cache with fetch
```typescript
// Cached by default in Next.js App Router (builds use static generation)
async function getProducts(): Promise<Product[]> {
const res = await fetch("https://api.example.com/products", {
// Revalidate every 60 seconds (ISR behavior)
next: { revalidate: 60 },
});
return res.json();
}
// Opt out of caching entirely
async function getCurrentUser(): Promise<User> {
const res = await fetch("https://api.example.com/me", {
cache: "no-store", // Always fetch fresh
headers: { Authorization: `Bearer ${getToken()}` },
});
return res.json();
}
```
#### Tag-based revalidation
```typescript
// Fetch with tags for targeted invalidation
async function getProduct(id: string): Promise<Product> {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: {
tags: [`product:${id}`, "products"],
},
});
return res.json();
}
// Server action to revalidate
"use server";
import { revalidateTag, revalidatePath } from "next/cache";
export async function updateProduct(id: string, data: ProductUpdate) {
await productApi.update(id, data);
// Revalidate specific product and the list
revalidateTag(`product:${id}`);
revalidateTag("products");
// Or revalidate an entire path
revalidatePath("/products");
}
```
#### unstable_cache for non-fetch data
```typescript
import { unstable_cache } from "next/cache";
// Cache database queries or any async function
const getCachedProducts = unstable_cache(
async (categoryId: string) => {
return await db.product.findMany({
where: { categoryId },
});
},
["products-by-category"], // Cache key prefix
{
revalidate: 300, // 5 minutes
tags: ["products"], // For manual invalidation
}
);
// Usage in a Server Component
export default async function ProductList({ categoryId }: Props) {
const products = await getCachedProducts(categoryId);
return <ProductGrid products={products} />;
}
```
#### Next.js cache layers summary
| Layer | What It Caches | Controlled By |
|-------|---------------|---------------|
| Request Memoization | Duplicate fetch calls in a single render | Automatic (same URL + options) |
| Data Cache | fetch responses on the server | `next: { revalidate }`, `cache: "no-store"` |
| Full Route Cache | Complete HTML and RSC payload | Static vs dynamic rendering |
| Router Cache | RSC payload in the browser | `revalidatePath`, `revalidateTag`, `router.refresh()` |
---
## Best Practices
1. **Cache at the right layer** — choose the caching layer closest to the consumer. HTTP caching eliminates network hops. CDN caching eliminates origin hits. Application caching eliminates database queries. Memoization eliminates repeated computation. Layer them, do not pick just one.
2. **Set TTLs based on data characteristics** — how stale can this data be before it causes a user-visible problem? Set TTL to that tolerance. A product description can be stale for minutes. An account balance cannot be stale at all.
3. **Use cache-aside as the default pattern** — read from cache, on miss fetch from source, populate cache. It is simple, handles cache failures gracefully (just hits the source), and keeps business logic decoupled from cache logic.
4. **Always set a max size or TTL** — unbounded caches cause memory leaks. Every cache should have either a size limit with eviction (LRU, LFU) or a TTL, or both. Monitor memory usage.
5. **Monitor cache hit rates** — a cache with a low hit rate is wasting memory without improving performance. Track hits, misses, and evictions. If the hit rate is below 80%, reconsider your key design or TTL.
6. **Design cache keys carefully** — keys should be deterministic, unique, and human-readable for debugging. Include a prefix identifying the data type, the relevant IDs, and optionally a version: `product:42:v3`, `user:123:profile`.
7. **Handle cache failures gracefully** — if Redis is down, fall through to the database. A cache failure should degrade performance, not break the application. Wrap cache calls in try/catch and log failures.
8. **Warm critical caches on startup** — for data that is expensive to fetch and always needed (configuration, feature flags, popular items), pre-populate the cache at application startup rather than waiting for the first user request to trigger a cold miss.
---
## Common Pitfalls
1. **Cache stampede (thundering herd)** — when a popular cache entry expires, hundreds of requests simultaneously hit the database to regenerate it. Mitigate with lock-based repopulation (only one request fetches, others wait), stale-while-revalidate (serve expired data while one request refreshes), or randomized TTLs (spread expiry times across a range).
```python
# Python — lock-based stampede prevention
async def get_with_lock(key: str, fetch_fn, ttl: int = 300) -> Any:
cached = await redis.get(key)
if cached is not None:
return json.loads(cached)
lock_key = f"lock:{key}"
acquired = await redis.set(lock_key, "1", ex=30, nx=True)
if acquired:
try:
value = await fetch_fn()
await redis.set(key, json.dumps(value, default=str), ex=ttl)
return value
finally:
await redis.delete(lock_key)
else:
# Another request is fetching — wait and retry
await asyncio.sleep(0.1)
return await get_with_lock(key, fetch_fn, ttl)
```
2. **Stale data in production** — a user updates their profile but keeps seeing old data because the cache was not invalidated. Always invalidate or update the cache in every write path. Use write-through caching for critical data, and test invalidation logic as carefully as you test the write itself.
3. **Caching errors** — if a database query fails and you cache the error response, every subsequent request gets the error until the TTL expires. Only cache successful results. Check the response before writing to cache.
```python
# Wrong — caches None on failure
result = await fetch_fn()
await redis.set(key, json.dumps(result), ex=ttl)
# Right — only cache valid results
result = await fetch_fn()
if result is not None:
await redis.set(key, json.dumps(result), ex=ttl)
```
4. **Over-memoizing in React** — wrapping every variable in `useMemo` and every function in `useCallback` adds overhead without benefit unless the value is passed to a memoized child component or is genuinely expensive to compute. Profile first, memoize second.
5. **Forgetting the Vary header** — if your API returns different content based on `Accept-Language` or `Accept-Encoding` but does not include `Vary`, the CDN may serve the wrong cached version to users. Always set `Vary` when response content depends on request headers.
6. **Cache key collisions** — using overly generic keys like `"products"` instead of `"products:category:5:page:2:sort:price"` causes different requests to share cached data. Include all parameters that affect the response in the cache key.
---
## 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
@@ -0,0 +1,201 @@
# Caching Decision Tree
## Primary Decision Tree
```
What are you caching?
├─ PURE FUNCTION RESULT (same input = same output)
│ │
│ ├─ In React component?
│ │ └─ useMemo(() => compute(data), [data])
│ │
│ ├─ Expensive computation called repeatedly?
│ │ └─ Memoize the function
│ │ Python: @functools.lru_cache or @functools.cache
│ │ JS: hand-rolled Map cache or lodash.memoize
│ │
│ └─ Shared across requests/processes?
│ └─ Use external cache (Redis) -- see below
├─ HTTP RESPONSE (browser or CDN caching)
│ │
│ ├─ Is it public (same for all users)?
│ │ │
│ │ ├─ Static asset (JS, CSS, images)?
│ │ │ └─ Cache-Control: public, max-age=31536000, immutable
│ │ │ (Use content hash in filename for busting)
│ │ │
│ │ ├─ API response that changes occasionally?
│ │ │ └─ Cache-Control: public, max-age=60, stale-while-revalidate=300
│ │ │ + ETag or Last-Modified for conditional requests
│ │ │
│ │ └─ HTML page?
│ │ └─ Cache-Control: public, max-age=0, must-revalidate
│ │ + ETag (let CDN/browser validate freshness)
│ │
│ └─ Is it private (user-specific)?
│ └─ Cache-Control: private, max-age=60
│ (Never cache auth tokens or sensitive data at CDN)
├─ DATABASE QUERY RESULT (shared across requests)
│ │
│ ├─ Read-heavy, rarely changes?
│ │ └─ Redis/Memcached with TTL
│ │ Pattern: Cache-aside (read-through)
│ │
│ ├─ Must always be fresh?
│ │ └─ Don't cache. Optimize the query instead.
│ │ (Add indexes, denormalize, materialized view)
│ │
│ └─ Needs real-time invalidation?
│ └─ Write-through cache or event-driven invalidation
│ (Update cache when DB changes)
├─ EXTERNAL API RESPONSE
│ │
│ ├─ API has rate limits?
│ │ └─ Cache aggressively. Respect Cache-Control from API.
│ │ Fallback: cache with reasonable TTL (5-60 min)
│ │
│ ├─ API is slow (>500ms)?
│ │ └─ Cache + stale-while-revalidate pattern
│ │ Serve stale, refresh in background
│ │
│ └─ API data is critical and must be fresh?
│ └─ Short TTL (10-30s) + circuit breaker on failure
└─ EDGE/CDN CACHING
├─ Global audience, same content?
│ └─ CDN with long TTL + purge on deploy
│ (Cloudflare, CloudFront, Vercel Edge)
├─ Personalized at edge?
│ └─ Edge compute (Cloudflare Workers, Vercel Edge Functions)
│ Cache shared parts, inject personalization
└─ A/B testing at edge?
└─ Vary by cookie or header
Vary: Cookie (careful: reduces cache hit rate)
```
---
## Cache-Aside Pattern (Most Common)
```
Read:
1. Check cache for key
2. HIT --> return cached value
3. MISS --> query DB, store in cache with TTL, return value
Write:
1. Update DB
2. Delete cache key (don't update -- avoids race conditions)
3. Next read will repopulate cache
```
```python
# Python + Redis
import redis, json
r = redis.Redis()
TTL = 300 # 5 minutes
def get_user(user_id: str) -> dict:
key = f"user:{user_id}"
cached = r.get(key)
if cached:
return json.loads(cached)
user = db.query("SELECT * FROM users WHERE id = %s", user_id)
r.setex(key, TTL, json.dumps(user))
return user
def update_user(user_id: str, data: dict):
db.execute("UPDATE users SET ... WHERE id = %s", user_id)
r.delete(f"user:{user_id}") # Invalidate, don't update
```
---
## TTL Strategy Guide
| Data Type | TTL | Rationale |
|-----------|-----|-----------|
| User session | 15-60 min | Balance security and UX |
| User profile | 5-15 min | Changes infrequently |
| Product catalog | 1-5 min | Needs reasonable freshness |
| Search results | 30s-2 min | Changes frequently |
| Static config | 1-24 hours | Rarely changes |
| Feature flags | 30s-1 min | Needs fast propagation |
| API rate limit counters | Match the rate limit window | Exact timing matters |
| Dashboard aggregations | 1-5 min | Expensive to compute |
### TTL Anti-Patterns
| Anti-Pattern | Problem | Fix |
|-------------|---------|-----|
| No TTL (cache forever) | Stale data, memory leak | Always set a TTL |
| TTL too short (<1s) | Cache provides no benefit | Remove cache or increase TTL |
| Same TTL for everything | Over/under-caching | Tune per data type |
| Stampede on expiry | All caches expire at once, DB overload | Jitter: TTL + random(0, 60s) |
---
## Cache Invalidation Strategies
| Strategy | How | Best For |
|----------|-----|----------|
| TTL expiry | Automatic, time-based | Most cases |
| Explicit delete | Delete key on write | Strong consistency needs |
| Write-through | Update cache on every write | Read-heavy, write-infrequent |
| Event-driven | Invalidate on DB change event | Microservices |
| Version key | Append version to cache key | Bulk invalidation |
| Tag-based | Group keys by tag, purge by tag | CDN, grouped content |
---
## Cache Headers Quick Reference
| Header | Example | Purpose |
|--------|---------|---------|
| `Cache-Control` | `max-age=3600` | Primary caching directive |
| `ETag` | `"abc123"` | Content fingerprint for conditional requests |
| `Last-Modified` | `Wed, 29 Jan 2025 12:00:00 GMT` | Timestamp for conditional requests |
| `Vary` | `Accept-Encoding, Authorization` | Cache varies by these headers |
| `CDN-Cache-Control` | `max-age=86400` | CDN-specific (Cloudflare, etc.) |
### Common Cache-Control Patterns
```
# Immutable static asset (hashed filename)
Cache-Control: public, max-age=31536000, immutable
# API data with background refresh
Cache-Control: public, max-age=60, stale-while-revalidate=300
# Private user data
Cache-Control: private, no-cache
# (no-cache = must revalidate, NOT "don't cache")
# Never cache
Cache-Control: no-store
# HTML pages (revalidate every time)
Cache-Control: public, max-age=0, must-revalidate
ETag: "content-hash-here"
```
---
## When NOT to Cache
| Scenario | Why |
|----------|-----|
| Data changes on every request | Cache hit rate ~0% |
| Data must be real-time consistent | Stale data is unacceptable |
| Write-heavy workload | Constant invalidation negates benefit |
| Data is cheap to compute/fetch | Cache overhead exceeds savings |
| Sensitive data (PII, financial) | Risk of serving wrong user's data |
| Early in development | Premature optimization; adds complexity |
@@ -0,0 +1,922 @@
---
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
@@ -0,0 +1,170 @@
# Error Taxonomy Quick Reference
## Two Fundamental Categories
### Operational Errors (Expected, Recoverable)
Errors that occur in correctly written programs due to external conditions.
**Response**: Handle gracefully. Log, retry, return error to user.
| Category | Description | HTTP Status | Example |
|----------|-------------|-------------|---------|
| Validation | Invalid input data | `400` | Missing required field, wrong format |
| Authentication | Identity not established | `401` | Missing/expired/invalid token |
| Authorization | Insufficient permissions | `403` | User lacks role for this action |
| Not Found | Resource does not exist | `404` | Item with given ID not in DB |
| Conflict | State conflict | `409` | Duplicate email, concurrent edit |
| Rate Limit | Too many requests | `429` | API quota exceeded |
| Payload Too Large | Request body too big | `413` | File upload exceeds limit |
| Unprocessable | Valid syntax, invalid semantics | `422` | Transfer amount exceeds balance |
| External Dependency | Third-party service failed | `502` / `503` | Payment gateway timeout |
| Service Unavailable | System overloaded or in maintenance | `503` | DB connection pool exhausted |
### Programmer Errors (Bugs, Fix the Code)
Errors caused by mistakes in the code itself.
**Response**: Fix the code. Do NOT catch and continue. Crash, log, alert.
| Category | Example | Fix |
|----------|---------|-----|
| TypeError | Calling method on undefined | Add null check or fix data flow |
| ReferenceError | Using undeclared variable | Fix variable name/scope |
| Assertion failure | Invariant violated | Fix logic that broke invariant |
| Wrong argument type | Passing string where number expected | Fix caller or add validation |
| Missing error handling | Unhandled promise rejection | Add try/catch or .catch() |
| Off-by-one | Array index out of bounds | Fix loop/index logic |
---
## Error Handling by Category
### Validation Errors (400)
```python
# Python/FastAPI
from pydantic import BaseModel, validator
class CreateUser(BaseModel):
email: str
age: int
@validator("age")
def validate_age(cls, v):
if v < 0 or v > 150:
raise ValueError("Age must be between 0 and 150")
return v
```
```typescript
// TypeScript
class ValidationError extends AppError {
constructor(public fields: Record<string, string>) {
super("Validation failed", 400);
}
}
```
### Authentication Errors (401)
| Scenario | Response | Action |
|----------|----------|--------|
| No token provided | 401 + `WWW-Authenticate` header | Client should authenticate |
| Token expired | 401 + error code `token_expired` | Client should refresh token |
| Token invalid | 401 + error code `invalid_token` | Client should re-authenticate |
### Not Found (404)
| Scenario | Use 404? |
|----------|----------|
| Resource by ID doesn't exist | Yes |
| Search returns no results | **No** -- return empty list with 200 |
| Resource soft-deleted | Depends on visibility rules |
| User lacks access to resource | Consider 403, or 404 to hide existence |
### Conflict (409)
| Scenario | Resolution |
|----------|------------|
| Duplicate unique field | Return which field conflicts |
| Optimistic locking failure | Return current version, client retries |
| State transition invalid | Return current state and valid transitions |
### External Dependency (502/503)
| Strategy | When |
|----------|------|
| Retry with backoff | Transient failures (timeouts, 503) |
| Circuit breaker | Repeated failures from same service |
| Fallback / degraded mode | Non-critical dependency |
| Queue for later | Async-compatible operations |
---
## Error Response Format
### Standard Error Response (JSON)
```json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{ "field": "email", "message": "Invalid email format" },
{ "field": "age", "message": "Must be a positive number" }
],
"request_id": "req_abc123"
}
}
```
### Error Code Convention
| Code Pattern | Meaning |
|-------------|---------|
| `VALIDATION_ERROR` | Input validation failed |
| `AUTH_TOKEN_EXPIRED` | Token needs refresh |
| `RESOURCE_NOT_FOUND` | Entity doesn't exist |
| `RATE_LIMIT_EXCEEDED` | Throttled |
| `CONFLICT_DUPLICATE` | Uniqueness violation |
| `INTERNAL_ERROR` | Unexpected server error (hide details) |
---
## Decision Guide
```
Error occurred
|
+--> Can the program continue safely?
| |
| +--> YES (operational error)
| | |
| | +--> Is it the client's fault? --> 4xx
| | +--> Is it our fault? --> 5xx
| | +--> Is it a dependency? --> 502/503
| |
| +--> NO (programmer error)
| |
| +--> Log full stack trace
| +--> Return generic 500 to client
| +--> Alert on-call
| +--> Fix the code
|
+--> Should the client see details?
|
+--> 4xx: Yes, help them fix their request
+--> 5xx: No, generic message + request_id for support
```
## Anti-Patterns
| Anti-Pattern | Why It's Bad | Instead |
|-------------|-------------|---------|
| Catch-all silently | Hides bugs | Catch specific errors, rethrow unknown |
| Return 200 with error body | Breaks HTTP semantics | Use proper status codes |
| Expose stack traces in prod | Security risk | Log internally, return request_id |
| String error matching | Fragile, breaks on message change | Use error codes/classes |
| Catch and log only | Request hangs or returns wrong data | Handle or propagate |
+962
View File
@@ -0,0 +1,962 @@
---
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
@@ -0,0 +1,157 @@
# Log Levels Quick Reference
## Level Summary
| Level | When to Use | Audience | Production Default |
|-------|------------|----------|-------------------|
| **DEBUG** | Detailed diagnostic info | Developers debugging | Off |
| **INFO** | Routine operational events | Ops team monitoring | On |
| **WARNING** | Something unexpected but handled | Ops + Devs | On |
| **ERROR** | Operation failed, needs attention | On-call engineers | On |
| **CRITICAL** | System is unusable or data at risk | On-call + management | On + alert |
---
## DEBUG
**Purpose**: Fine-grained information useful only when diagnosing problems.
**Turn on**: During local development or when investigating a specific issue.
| Good | Bad |
|------|-----|
| `Parsing config file: /etc/app/config.yaml` | `Entering function parse_config` |
| `Cache miss for key user:123, fetching from DB` | `x = 5` |
| `SQL: SELECT * FROM users WHERE id=$1 [params: 123]` | `Here we go!` |
| `Retry attempt 2/3 for payment gateway` | `Debug debug debug` |
| `JWT token expires at 2025-01-29T10:00:00Z` | `token = eyJhbG...` (secret!) |
**Rule**: Never log secrets, tokens, passwords, or PII at any level.
---
## INFO
**Purpose**: Confirm the system is working as expected. Key business events.
| Good | Bad |
|------|-----|
| `Server started on port 8080` | `Server is running` (which port? which version?) |
| `User user:456 created account via OAuth (Google)` | `New user` |
| `Order ord:789 placed, total=$45.00, items=3` | `Order created` |
| `Migration v42 applied successfully (12 tables)` | `Migration done` |
| `Scheduled job "daily-report" completed in 4.2s` | `Job finished` |
| `Payment processed: txn:abc, amount=$99, method=card` | `Payment OK` |
**Rule**: Include enough context to answer "what happened, to what, and relevant numbers."
---
## WARNING
**Purpose**: Something unexpected happened, but the system handled it. May indicate a future problem.
| Good | Bad |
|------|-----|
| `Connection pool at 85% capacity (17/20)` | `Pool getting full` |
| `Deprecated API v1 called by client app:legacy (use v2)` | `Old API used` |
| `Disk space below 10% on /data (2.1 GB remaining)` | `Low disk` |
| `Request took 4.8s (threshold: 5s) for GET /api/search` | `Slow request` |
| `Config REDIS_URL missing, falling back to in-memory cache` | `No Redis` |
| `Rate limit approaching for IP 10.0.0.5: 90/100 requests` | `Almost rate limited` |
**Rule**: Warnings should be actionable. If nobody would investigate, it's DEBUG or INFO.
---
## ERROR
**Purpose**: An operation failed. The system can continue, but something broke.
| Good | Bad |
|------|-----|
| `Failed to send email to user:123: SMTP timeout after 30s` | `Email error` |
| `Payment declined for order:789: card_expired (Stripe)` | `Payment failed` |
| `Database query timeout after 10s: SELECT FROM orders WHERE...` | `DB error` |
| `File upload failed: S3 returned 503, bucket=media, key=img/456.jpg` | `Upload error` |
| `Unhandled exception in POST /api/orders: ValueError("...")` | (stack trace only, no context) |
**Rule**: Include the operation, the target/ID, the error detail, and what was attempted.
---
## CRITICAL
**Purpose**: System is unusable or data integrity is at risk. Requires immediate human intervention.
| Good | Bad |
|------|-----|
| `Database connection lost, all pools exhausted, 0/20 available` | `DB down` |
| `Disk full on /data, writes failing, data loss possible` | `No disk space` |
| `Security: 500 failed login attempts from IP 10.0.0.5 in 60s` | `Too many logins` |
| `Data corruption detected: order:789 total=-$50.00` | `Bad data` |
| `TLS certificate expires in 24h, auto-renewal failed` | `Cert expiring` |
**Rule**: Every CRITICAL log should trigger an alert (PagerDuty, Slack, etc.).
---
## Structured Logging Format
### Python (structlog)
```python
import structlog
log = structlog.get_logger()
log.info("order.placed", order_id="ord:789", total=45.00, items=3)
log.error("email.send_failed", user_id="user:123", error="SMTP timeout", retry=2)
```
### TypeScript (pino)
```typescript
import pino from "pino";
const log = pino({ level: "info" });
log.info({ orderId: "ord:789", total: 45.0, items: 3 }, "order.placed");
log.error({ userId: "user:123", err, retry: 2 }, "email.send_failed");
```
### Key-Value Best Practices
| Field | Purpose | Example |
|-------|---------|---------|
| `event` / message | What happened | `"order.placed"` |
| `request_id` | Trace across services | `"req_abc123"` |
| `user_id` | Who triggered it | `"user:456"` |
| `duration_ms` | How long it took | `142` |
| `error` | Error message (not stack in prod) | `"connection refused"` |
| `component` | Which module/service | `"payment-gateway"` |
---
## Configuration by Environment
| Environment | Minimum Level | Structured? | Destination |
|-------------|--------------|-------------|-------------|
| Local dev | DEBUG | No (human-readable) | stdout |
| CI/Test | WARNING | No | stdout |
| Staging | DEBUG | Yes (JSON) | Log aggregator |
| Production | INFO | Yes (JSON) | Log aggregator |
---
## Anti-Patterns
| Anti-Pattern | Problem | Fix |
|-------------|---------|-----|
| Logging PII/secrets | Security/compliance violation | Redact or mask sensitive fields |
| `log.error()` in a loop | Log flooding, storage cost | Log once with count |
| `log.error("Error: " + err)` | Missing context, hard to search | Use structured fields |
| Logging at wrong level | Alert fatigue or missed issues | Follow the guide above |
| Catch-log-rethrow | Duplicate log entries | Log at the handling site only |
| No request_id | Cannot correlate logs | Add correlation ID middleware |
| Logging full request bodies | Performance, storage, PII risk | Log summary fields only |
@@ -0,0 +1,782 @@
---
name: state-management
description: >
State management patterns for React and Python applications. Use this skill when choosing between useState, useReducer, context, Zustand, Jotai, or TanStack Query. Also applies to server state, form state, URL state, and Python application state with dataclasses and Pydantic. Trigger whenever someone asks about state architecture, global state, caching API responses, or managing complex form state.
---
# State Management Patterns
## When to Use
- Choosing between local, shared, or global state in a React application
- Setting up server state caching with TanStack Query or SWR
- Building forms with validation, arrays, and nested fields
- Syncing application state with URL search parameters
- Designing Python domain models with dataclasses or Pydantic
- Refactoring prop-drilling into a shared store
- Deciding whether to add a state management library or keep things simple
## When NOT to Use
- Static sites with no interactive state (pure content pages, docs)
- Server-only rendering with no client-side interactivity
- Simple CRUD backends where database is the only source of truth and there is no in-process state to manage
---
## Core Patterns
### 1. Local vs Global State Decision Tree
Before reaching for a library, walk through this decision tree.
```
Is the state used by a single component?
├── YES --> useState or useReducer
└── NO
Is it shared by a parent and 1-2 direct children?
├── YES --> Lift state up to the common parent, pass via props
└── NO
Is it server data (fetched from an API)?
├── YES --> TanStack Query (useQuery / useMutation)
└── NO
Is it URL-representable (filters, pagination, tabs)?
├── YES --> URL state (useSearchParams / nuqs)
└── NO
Is it form data with validation?
├── YES --> react-hook-form + zod
└── NO
Zustand store (or Jotai for atomic state)
```
**Rules of thumb:**
- Start with the simplest option. Only add a library when props become painful.
- Server state and client state are different concerns. Never put fetched API data in Zustand; use TanStack Query instead.
- URL state is free persistence. If the user should be able to bookmark or share the current view, put it in the URL.
- Form state belongs to the form library. Do not mirror react-hook-form values in a Zustand store.
---
### 2. React State Patterns
**useState for simple values**
```typescript
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((prev) => prev + 1)}>
Count: {count}
</button>
);
}
```
**useReducer for complex state with multiple transitions**
```typescript
interface TimerState {
status: "idle" | "running" | "paused";
elapsed: number;
}
type TimerAction =
| { type: "start" }
| { type: "pause" }
| { type: "reset" }
| { type: "tick" };
function timerReducer(state: TimerState, action: TimerAction): TimerState {
switch (action.type) {
case "start":
return { ...state, status: "running" };
case "pause":
return { ...state, status: "paused" };
case "reset":
return { status: "idle", elapsed: 0 };
case "tick":
return state.status === "running"
? { ...state, elapsed: state.elapsed + 1 }
: state;
}
}
function Timer() {
const [state, dispatch] = useReducer(timerReducer, {
status: "idle",
elapsed: 0,
});
useEffect(() => {
if (state.status !== "running") return;
const id = setInterval(() => dispatch({ type: "tick" }), 1000);
return () => clearInterval(id);
}, [state.status]);
return (
<div>
<p>{state.elapsed}s</p>
{state.status !== "running" && (
<button onClick={() => dispatch({ type: "start" })}>Start</button>
)}
{state.status === "running" && (
<button onClick={() => dispatch({ type: "pause" })}>Pause</button>
)}
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}
```
**When to pick which:**
| Criteria | useState | useReducer |
|----------|----------|------------|
| Single primitive value | Yes | Overkill |
| Multiple related fields | Possible | Preferred |
| Complex transitions | Messy | Clean |
| Needs testing in isolation | Hard | Easy (test the reducer) |
---
### 3. Global State (Zustand)
Zustand is lightweight, TypeScript-friendly, and avoids the boilerplate of Redux.
**Basic store**
```typescript
import { create } from "zustand";
interface AuthStore {
user: User | null;
token: string | null;
login: (user: User, token: string) => void;
logout: () => void;
}
const useAuthStore = create<AuthStore>((set) => ({
user: null,
token: null,
login: (user, token) => set({ user, token }),
logout: () => set({ user: null, token: null }),
}));
// In components - use selectors to avoid unnecessary re-renders
function UserMenu() {
const user = useAuthStore((s) => s.user);
const logout = useAuthStore((s) => s.logout);
if (!user) return <LoginButton />;
return (
<div>
<span>{user.name}</span>
<button onClick={logout}>Log out</button>
</div>
);
}
```
**Slices pattern for large stores**
```typescript
import { create, type StateCreator } from "zustand";
import { devtools, persist } from "zustand/middleware";
// Each slice is its own interface + creator
interface CartSlice {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clearCart: () => void;
}
interface UISlice {
sidebarOpen: boolean;
toggleSidebar: () => void;
}
const createCartSlice: StateCreator<
CartSlice & UISlice,
[],
[],
CartSlice
> = (set) => ({
items: [],
addItem: (item) =>
set((state) => ({ items: [...state.items, item] })),
removeItem: (id) =>
set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
clearCart: () => set({ items: [] }),
});
const createUISlice: StateCreator<
CartSlice & UISlice,
[],
[],
UISlice
> = (set) => ({
sidebarOpen: false,
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
});
// Combine slices with middleware
const useAppStore = create<CartSlice & UISlice>()(
devtools(
persist(
(...args) => ({
...createCartSlice(...args),
...createUISlice(...args),
}),
{
name: "app-store",
partialize: (state) => ({ items: state.items }), // only persist cart
}
)
)
);
```
**Zustand best practices:**
- Always use selectors (`useStore((s) => s.field)`) instead of the whole store.
- Keep stores small and focused. One store per domain, not one mega-store.
- Use `persist` middleware for state that should survive page reloads.
- Use `devtools` middleware in development for Redux DevTools integration.
---
### 4. Server State (TanStack Query)
Server state (data from APIs) has different needs than client state: caching, background refetching, deduplication, pagination.
**Basic query**
```typescript
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
// Query key conventions: [entity, ...params]
const userKeys = {
all: ["users"] as const,
list: (filters: UserFilters) => ["users", "list", filters] as const,
detail: (id: string) => ["users", "detail", id] as const,
};
function useUser(id: string) {
return useQuery({
queryKey: userKeys.detail(id),
queryFn: () => api.users.get(id),
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
function UserProfile({ id }: { id: string }) {
const { data: user, isLoading, error } = useUser(id);
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
return <div>{user.name}</div>;
}
```
**Mutations with optimistic updates**
```typescript
function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: UpdateUserInput) => api.users.update(data.id, data),
onMutate: async (newData) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({
queryKey: userKeys.detail(newData.id),
});
// Snapshot current value for rollback
const previous = queryClient.getQueryData(userKeys.detail(newData.id));
// Optimistically update
queryClient.setQueryData(userKeys.detail(newData.id), (old: User) => ({
...old,
...newData,
}));
return { previous };
},
onError: (_err, newData, context) => {
// Rollback on failure
if (context?.previous) {
queryClient.setQueryData(
userKeys.detail(newData.id),
context.previous
);
}
},
onSettled: (_data, _err, variables) => {
// Always refetch after mutation to ensure consistency
queryClient.invalidateQueries({
queryKey: userKeys.detail(variables.id),
});
},
});
}
```
**Prefetching for instant navigation**
```typescript
function UserListItem({ user }: { user: UserSummary }) {
const queryClient = useQueryClient();
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: userKeys.detail(user.id),
queryFn: () => api.users.get(user.id),
staleTime: 60_000,
});
};
return (
<Link
to={`/users/${user.id}`}
onMouseEnter={prefetch}
onFocus={prefetch}
>
{user.name}
</Link>
);
}
```
---
### 5. Form State
Use react-hook-form for performance (uncontrolled inputs) and zod for schema validation.
**Basic form with validation**
```typescript
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const createUserSchema = z.object({
name: z.string().min(1, "Name is required").max(100),
email: z.string().email("Invalid email address"),
role: z.enum(["admin", "editor", "viewer"]),
notifications: z.boolean().default(true),
});
type CreateUserForm = z.infer<typeof createUserSchema>;
function CreateUserForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<CreateUserForm>({
resolver: zodResolver(createUserSchema),
defaultValues: {
role: "viewer",
notifications: true,
},
});
const onSubmit = async (data: CreateUserForm) => {
await api.users.create(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="name">Name</label>
<input id="name" {...register("name")} />
{errors.name && <p className="text-red-500">{errors.name.message}</p>}
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" type="email" {...register("email")} />
{errors.email && <p className="text-red-500">{errors.email.message}</p>}
</div>
<div>
<label htmlFor="role">Role</label>
<select id="role" {...register("role")}>
<option value="viewer">Viewer</option>
<option value="editor">Editor</option>
<option value="admin">Admin</option>
</select>
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Creating..." : "Create User"}
</button>
</form>
);
}
```
**Dynamic field arrays**
```typescript
import { useFieldArray } from "react-hook-form";
const orderSchema = z.object({
customer: z.string().min(1),
items: z
.array(
z.object({
productId: z.string().min(1),
quantity: z.number().int().min(1).max(999),
})
)
.min(1, "At least one item is required"),
});
type OrderForm = z.infer<typeof orderSchema>;
function OrderForm() {
const { register, control, handleSubmit } = useForm<OrderForm>({
resolver: zodResolver(orderSchema),
defaultValues: { items: [{ productId: "", quantity: 1 }] },
});
const { fields, append, remove } = useFieldArray({
control,
name: "items",
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => (
<div key={field.id} className="flex gap-2">
<input
{...register(`items.${index}.productId`)}
placeholder="Product ID"
/>
<input
{...register(`items.${index}.quantity`, { valueAsNumber: true })}
type="number"
min={1}
/>
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
))}
<button
type="button"
onClick={() => append({ productId: "", quantity: 1 })}
>
Add item
</button>
<button type="submit">Place order</button>
</form>
);
}
```
---
### 6. URL State
Encode filters, pagination, and view settings in the URL so users can bookmark and share.
**Using nuqs (type-safe URL search params)**
```typescript
import { useQueryState, parseAsInteger, parseAsStringEnum } from "nuqs";
const sortOptions = ["name", "date", "price"] as const;
function ProductList() {
const [search, setSearch] = useQueryState("q", { defaultValue: "" });
const [page, setPage] = useQueryState("page", parseAsInteger.withDefault(1));
const [sort, setSort] = useQueryState(
"sort",
parseAsStringEnum(sortOptions).withDefault("name")
);
// URL looks like: /products?q=shoes&page=2&sort=price
const { data } = useQuery({
queryKey: ["products", { search, page, sort }],
queryFn: () => api.products.list({ search, page, sort }),
});
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value || null)}
placeholder="Search products..."
/>
<select
value={sort}
onChange={(e) => setSort(e.target.value as typeof sort)}
>
{sortOptions.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
<ProductGrid products={data?.items ?? []} />
<Pagination
page={page}
total={data?.totalPages ?? 1}
onChange={setPage}
/>
</div>
);
}
```
**Using React Router useSearchParams**
```typescript
import { useSearchParams } from "react-router-dom";
function FilteredList() {
const [searchParams, setSearchParams] = useSearchParams();
const status = searchParams.get("status") ?? "all";
const page = Number(searchParams.get("page") ?? "1");
const updateFilter = (key: string, value: string | null) => {
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
if (value === null) {
next.delete(key);
} else {
next.set(key, value);
}
// Reset page when filter changes
if (key !== "page") next.set("page", "1");
return next;
});
};
return (
<div>
<select
value={status}
onChange={(e) => updateFilter("status", e.target.value)}
>
<option value="all">All</option>
<option value="active">Active</option>
<option value="archived">Archived</option>
</select>
</div>
);
}
```
---
### 7. Python State
Use dataclasses for lightweight domain objects and Pydantic for validated external data. Combine with the repository pattern for persistence.
**Dataclasses for domain objects**
```python
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from uuid import UUID, uuid4
class OrderStatus(str, Enum):
DRAFT = "draft"
CONFIRMED = "confirmed"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
@dataclass
class OrderItem:
product_id: str
quantity: int
unit_price: float
@property
def total(self) -> float:
return self.quantity * self.unit_price
@dataclass
class Order:
customer_id: str
items: list[OrderItem] = field(default_factory=list)
id: UUID = field(default_factory=uuid4)
status: OrderStatus = OrderStatus.DRAFT
created_at: datetime = field(default_factory=datetime.utcnow)
@property
def subtotal(self) -> float:
return sum(item.total for item in self.items)
def confirm(self) -> None:
if self.status != OrderStatus.DRAFT:
raise ValueError(f"Cannot confirm order in '{self.status}' state")
if not self.items:
raise ValueError("Cannot confirm an empty order")
self.status = OrderStatus.CONFIRMED
def cancel(self) -> None:
if self.status in (OrderStatus.DELIVERED, OrderStatus.CANCELLED):
raise ValueError(f"Cannot cancel order in '{self.status}' state")
self.status = OrderStatus.CANCELLED
```
**Pydantic for validated external input**
```python
from pydantic import BaseModel, Field, field_validator
class CreateOrderRequest(BaseModel):
customer_id: str = Field(min_length=1, max_length=50)
items: list["OrderItemInput"] = Field(min_length=1)
@field_validator("items")
@classmethod
def no_duplicate_products(cls, items: list["OrderItemInput"]) -> list["OrderItemInput"]:
product_ids = [item.product_id for item in items]
if len(product_ids) != len(set(product_ids)):
raise ValueError("Duplicate product IDs are not allowed")
return items
class OrderItemInput(BaseModel):
product_id: str = Field(min_length=1)
quantity: int = Field(ge=1, le=999)
```
**Repository pattern for persistence**
```python
from abc import ABC, abstractmethod
class OrderRepository(ABC):
@abstractmethod
async def save(self, order: Order) -> None: ...
@abstractmethod
async def get(self, order_id: UUID) -> Order | None: ...
@abstractmethod
async def list_by_customer(self, customer_id: str) -> list[Order]: ...
class PostgresOrderRepository(OrderRepository):
def __init__(self, pool) -> None:
self.pool = pool
async def save(self, order: Order) -> None:
async with self.pool.acquire() as conn:
await conn.execute(
"""
INSERT INTO orders (id, customer_id, status, created_at)
VALUES ($1, $2, $3, $4)
ON CONFLICT (id) DO UPDATE SET status = $3
""",
order.id,
order.customer_id,
order.status.value,
order.created_at,
)
# Upsert items...
async def get(self, order_id: UUID) -> Order | None:
async with self.pool.acquire() as conn:
row = await conn.fetchrow(
"SELECT * FROM orders WHERE id = $1", order_id
)
if row is None:
return None
items = await conn.fetch(
"SELECT * FROM order_items WHERE order_id = $1", order_id
)
return self._row_to_order(row, items)
async def list_by_customer(self, customer_id: str) -> list[Order]:
async with self.pool.acquire() as conn:
rows = await conn.fetch(
"SELECT * FROM orders WHERE customer_id = $1 ORDER BY created_at DESC",
customer_id,
)
# Fetch items for each order...
return [self._row_to_order(row, items) for row, items in results]
def _row_to_order(self, row, item_rows) -> Order:
return Order(
id=row["id"],
customer_id=row["customer_id"],
status=OrderStatus(row["status"]),
created_at=row["created_at"],
items=[
OrderItem(
product_id=r["product_id"],
quantity=r["quantity"],
unit_price=float(r["unit_price"]),
)
for r in item_rows
],
)
```
---
## Best Practices
1. **Start local, promote when needed.** Begin with `useState`. Only move state up or into a store when two or more unrelated components need the same data. Premature globalization makes refactoring painful.
2. **Separate server state from client state.** Use TanStack Query (or SWR) for anything fetched from an API. These libraries handle caching, deduplication, background refetching, and stale-while-revalidate. Do not duplicate fetched data in Zustand.
3. **Use selectors to prevent re-renders.** In Zustand, always select the specific field you need: `useStore((s) => s.count)`, not `useStore()`. The latter re-renders on every store change.
4. **Co-locate state with the component that owns it.** If only `<Sidebar>` uses `isOpen`, keep that state inside `<Sidebar>`. Moving it to a global store just because "it might be needed later" creates unnecessary coupling.
5. **Derive, do not duplicate.** If `fullName` can be computed from `firstName` and `lastName`, compute it on the fly or with `useMemo`. Storing derived values introduces synchronization bugs.
6. **Validate at the boundary, trust internally.** Use Pydantic or Zod to validate data when it enters the system (API requests, form submissions, external events). Once validated, pass typed objects without re-checking.
7. **Keep URL state minimal.** Only encode values the user would want to bookmark or share: active tab, search query, page number, sort column. Do not put ephemeral UI state (hover, open dropdown) in the URL.
8. **Treat form state as its own domain.** Let react-hook-form manage form values, dirty tracking, and validation. Submit the validated result to your mutation or API call. Do not synchronize form fields with external stores.
---
## Common Pitfalls
1. **Putting everything in global state.** Not all state needs to be global. A modal's open/closed state, an input's current text, or a component's loading spinner should stay local. Global stores should hold state that genuinely needs to be shared across distant parts of the tree.
2. **Storing server data in Zustand.** Zustand has no built-in cache invalidation, stale detection, or background refetch. Using it for API data means you are rebuilding TanStack Query poorly. Use the right tool for the job.
3. **Forgetting to invalidate queries after mutations.** After a `useMutation` succeeds, call `queryClient.invalidateQueries` with the affected keys. Without this, the UI shows stale data until the next refetch interval.
4. **Over-using React Context for frequently changing state.** Every Context value change re-renders every consumer. Context is good for low-frequency values (theme, locale, auth). For high-frequency updates (cursor position, scroll offset), use Zustand or a ref.
5. **Duplicating form state.** Calling `useForm()` and then also storing the same values in `useState` or Zustand means two sources of truth that can drift apart. Let the form library be the single owner.
6. **Ignoring URL state for filterable lists.** If a user applies filters and then hits the back button or refreshes, losing the filters is a bad experience. Encode filters in the URL so they survive navigation.
---
## Related Skills
- `frameworks/react` - React component patterns and hooks
- `frameworks/nextjs` - Next.js server components and data fetching
- `languages/typescript` - TypeScript types and generics
- `patterns/caching` - Cache strategies and invalidation patterns
@@ -0,0 +1,143 @@
# State Management Decision Tree
## Primary Decision Tree
```
What kind of state is it?
├─ SERVER DATA (fetched from API/DB)
│ │
│ ├─ React/Next.js project?
│ │ ├─ YES ──> TanStack Query (React Query)
│ │ │ - Auto caching, dedup, background refresh
│ │ │ - Stale-while-revalidate out of the box
│ │ │ - DevTools for debugging
│ │ │
│ │ └─ Next.js App Router with Server Components?
│ │ └─ Consider: fetch() in Server Components + revalidation
│ │ - No client-side state library needed
│ │ - Use TanStack Query only for client-interactive data
│ │
│ └─ Need real-time sync?
│ ├─ WebSocket data ──> TanStack Query + custom subscription
│ └─ Collaborative ──> Liveblocks, Yjs, or Partykit
├─ URL STATE (filters, pagination, search, tabs)
│ │
│ ├─ Next.js App Router ──> useSearchParams() + useRouter()
│ ├─ React Router ──> useSearchParams()
│ └─ Plain React ──> nuqs or custom URL sync hook
│ Why URL state? Shareable links, back/forward navigation,
│ bookmarkable, SSR-friendly.
├─ FORM STATE (input values, validation, dirty/touched)
│ │
│ ├─ Complex forms (multi-step, dynamic fields, arrays)
│ │ └─ react-hook-form + zod
│ │ - Uncontrolled by default (performant)
│ │ - Schema validation with zod resolver
│ │
│ ├─ Simple forms (login, search, contact)
│ │ └─ Server Actions (Next.js) or native form + useState
│ │
│ └─ Form + server state (edit existing record)
│ └─ TanStack Query (fetch) + react-hook-form (edit)
│ - Populate form with query data
│ - Submit with mutation
├─ GLOBAL CLIENT STATE (shared across many components)
│ │
│ ├─ Simple (theme, sidebar open, user preferences)
│ │ │
│ │ ├─ Changes rarely ──> React Context
│ │ │ (Wrap app, useContext to consume)
│ │ │
│ │ └─ Changes often or many consumers ──> Zustand
│ │ - Avoids Context re-render problem
│ │ - Selector-based subscriptions
│ │ - Tiny bundle, minimal boilerplate
│ │
│ ├─ Complex (shopping cart, multi-step wizard, editor)
│ │ └─ Zustand (or Jotai for atomic state)
│ │
│ └─ Need devtools and time-travel debugging?
│ └─ Zustand with devtools middleware
├─ LOCAL COMPONENT STATE (only used in one component)
│ │
│ ├─ Single value ──> useState
│ │ const [count, setCount] = useState(0);
│ │
│ ├─ Related values or complex transitions ──> useReducer
│ │ const [state, dispatch] = useReducer(reducer, initial);
│ │
│ └─ Derived value (computed from other state) ──> useMemo
│ const total = useMemo(() => items.reduce(...), [items]);
└─ TRANSIENT UI STATE (animations, hover, drag position)
├─ CSS can handle it? ──> Use CSS (transitions, :hover)
├─ Ref-based (no re-render needed) ──> useRef
└─ Needs re-render ──> useState (local)
```
## Quick Lookup Table
| State Type | Recommended Tool | When NOT to Use |
|-----------|-----------------|-----------------|
| Server data | TanStack Query | Data never changes, or SSR-only |
| URL params | useSearchParams | Ephemeral UI state (hover, etc.) |
| Form inputs | react-hook-form | Single `<input>` |
| Global UI | Zustand | Only 1-2 consumers (use Context) |
| Global UI (simple) | React Context | Frequent updates with many consumers |
| Local state | useState | Complex state transitions |
| Complex local | useReducer | Single boolean toggle |
| Derived data | useMemo | Cheap computations |
| No re-render needed | useRef | Value that should trigger re-render |
## Library Comparison
| Library | Bundle Size | Boilerplate | Learning Curve | Best For |
|---------|------------|-------------|----------------|----------|
| useState/useReducer | 0 KB | Minimal | Low | Local state |
| React Context | 0 KB | Low | Low | Rarely-changing global state |
| Zustand | ~1 KB | Minimal | Low | Global client state |
| Jotai | ~3 KB | Minimal | Medium | Atomic/derived state |
| TanStack Query | ~12 KB | Medium | Medium | Server state |
| Redux Toolkit | ~30 KB | High | High | Large teams needing strict patterns |
## Common Mistakes
| Mistake | Problem | Fix |
|---------|---------|-----|
| Storing server data in Zustand/Redux | Manual cache invalidation, stale data | Use TanStack Query |
| Storing URL state in useState | Not shareable, lost on refresh | Use URL search params |
| Putting everything in global state | Unnecessary re-renders, complexity | Colocate state where used |
| Context for frequently changing data | Re-renders all consumers | Use Zustand with selectors |
| Duplicating derived state | Out-of-sync bugs | Compute with useMemo |
| useState for complex transitions | Inconsistent intermediate states | Use useReducer |
## Zustand Quick Setup
```typescript
import { create } from "zustand";
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
total: () => number;
}
const useCartStore = create<CartStore>((set, get) => ({
items: [],
addItem: (item) => set((s) => ({ items: [...s.items, item] })),
removeItem: (id) => set((s) => ({ items: s.items.filter(i => i.id !== id) })),
total: () => get().items.reduce((sum, i) => sum + i.price, 0),
}));
// Usage with selector (only re-renders when items change)
const items = useCartStore((s) => s.items);
const addItem = useCartStore((s) => s.addItem);
```
+524 -42
View File
@@ -1,74 +1,556 @@
# OWASP Security
---
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.
---
## Description
OWASP Top 10 security practices and secure coding patterns.
# OWASP Web Application Security
## When to Use
- Security code reviews
- Implementing authentication
- Handling user input
- Implementing authentication or authorization
- Handling user input from untrusted sources
- Building or auditing web API endpoints
- Configuring CORS, CSP, or other security headers
- Managing secrets, tokens, or credentials in code
- Setting up rate limiting or brute force protection
## When NOT to Use
- General code style or formatting reviews with no security implications
- Non-web applications such as CLI tools, batch scripts, or desktop utilities
- Performance optimization tasks where security is not the concern
- Infrastructure-level security (firewall rules, network segmentation)
---
## Core Patterns
### Input Validation
### 1. Input Validation & Sanitization
Always validate input at the boundary. Use allowlists over denylists.
**Python (Pydantic)**
```python
# Always validate and sanitize
from pydantic import BaseModel, EmailStr
# BAD - no validation, accepts anything
@app.post("/users")
async def create_user(request: Request):
data = await request.json()
name = data["name"] # no length check, no type check
email = data["email"] # no format validation
role = data["role"] # user controls their own role
db.execute(f"INSERT INTO users VALUES ('{name}', '{email}', '{role}')")
class UserInput(BaseModel):
# GOOD - strict schema validation with Pydantic
from pydantic import BaseModel, EmailStr, Field
from enum import Enum
class UserRole(str, Enum):
viewer = "viewer"
editor = "editor"
class CreateUserRequest(BaseModel):
name: str = Field(min_length=1, max_length=100, pattern=r"^[a-zA-Z\s\-]+$")
email: EmailStr
name: str = Field(min_length=1, max_length=100)
role: UserRole = UserRole.viewer # default to least privilege
@app.post("/users")
async def create_user(payload: CreateUserRequest):
# Pydantic rejects invalid data before this code runs
db.add(User(name=payload.name, email=payload.email, role=payload.role))
```
### SQL Injection Prevention
```python
# Never concatenate user input
# Bad
query = f"SELECT * FROM users WHERE id = {user_id}"
# Good - parameterized
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
```
### XSS Prevention
**TypeScript (Zod)**
```typescript
// Never use innerHTML with user data
// Bad
element.innerHTML = userInput;
// BAD - trusting req.body directly
app.post("/users", (req, res) => {
const { name, email, role } = req.body; // no validation
db.query(`INSERT INTO users VALUES ('${name}', '${email}', '${role}')`);
});
// Good
element.textContent = userInput;
// GOOD - validate with Zod at the boundary
import { z } from "zod";
const CreateUserSchema = z.object({
name: z.string().min(1).max(100).regex(/^[a-zA-Z\s\-]+$/),
email: z.string().email(),
role: z.enum(["viewer", "editor"]).default("viewer"),
});
app.post("/users", (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
// result.data is typed and validated
await prisma.user.create({ data: result.data });
});
```
### Authentication
**File Upload Validation**
```python
# Hash passwords properly
# GOOD - validate MIME type (not just extension), size, and sanitize filename
import magic
ALLOWED_TYPES = {"image/jpeg", "image/png", "application/pdf"}
MAX_SIZE = 5 * 1024 * 1024 # 5 MB
def validate_upload(file_bytes: bytes, filename: str) -> bool:
if len(file_bytes) > MAX_SIZE:
raise ValueError("File too large")
if magic.from_buffer(file_bytes, mime=True) not in ALLOWED_TYPES:
raise ValueError("Disallowed file type")
if ".." in filename or filename.startswith("."):
raise ValueError("Invalid filename")
return True
```
### 2. SQL Injection Prevention
Never concatenate user input into SQL strings. Always use parameterized queries or ORM methods.
**Raw SQL (Python)**
```python
# BAD - string interpolation creates injection vector
def get_user(user_id: str):
query = f"SELECT * FROM users WHERE id = '{user_id}'"
# Input: "'; DROP TABLE users; --" destroys the table
cursor.execute(query)
# GOOD - parameterized query
def get_user(user_id: str):
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
return cursor.fetchone()
```
**SQLAlchemy (Python)**
```python
# BAD - text() with f-string
from sqlalchemy import text
result = session.execute(text(f"SELECT * FROM users WHERE name = '{name}'"))
# GOOD - bound parameters with text()
result = session.execute(text("SELECT * FROM users WHERE name = :name"), {"name": name})
# GOOD - ORM query (automatically parameterized)
user = session.query(User).filter(User.name == name).first()
```
**Prisma (TypeScript)**
```typescript
// BAD - raw query with interpolation
const user = await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE id = '${id}'`);
// GOOD - tagged template (auto-parameterized)
const user = await prisma.$queryRaw`SELECT * FROM users WHERE id = ${id}`;
// GOOD - Prisma client methods (always safe)
const user = await prisma.user.findUnique({ where: { id } });
```
### 3. XSS Prevention
Prevent cross-site scripting by encoding output, setting CSP headers, and sanitizing HTML.
**Output Encoding**
```typescript
// BAD - renders raw user content as HTML
element.innerHTML = userComment;
// GOOD - use textContent for plain text
element.textContent = userComment;
// GOOD - React auto-escapes by default (don't bypass it)
return <div>{userComment}</div>;
// BAD - dangerouslySetInnerHTML defeats React's protection
return <div dangerouslySetInnerHTML={{ __html: userComment }} />;
```
**Sanitizing HTML When You Must Render It**
```typescript
// GOOD - sanitize with DOMPurify when HTML rendering is required
import DOMPurify from "dompurify";
const cleanHtml = DOMPurify.sanitize(userHtml, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br"],
ALLOWED_ATTR: ["href", "title"],
});
return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
```
### 4. Authentication Patterns
**Password Hashing**
```python
# BAD - plain text or weak hashing
hashed = hashlib.md5(password.encode()).hexdigest() # trivially crackable
# GOOD - use argon2 (preferred) or bcrypt with proper cost
from passlib.hash import argon2
hashed = argon2.hash(password)
verified = argon2.verify(password, hashed)
is_valid = argon2.verify(password, hashed)
```
## Security Checklist
```typescript
// GOOD - bcrypt in Node.js
import bcrypt from "bcrypt";
- [ ] Input validation on all user data
- [ ] Parameterized queries
- [ ] Output encoding
- [ ] Strong password hashing
- [ ] Secure session management
- [ ] HTTPS everywhere
- [ ] Security headers configured
const SALT_ROUNDS = 12;
const hashed = await bcrypt.hash(password, SALT_ROUNDS);
const isValid = await bcrypt.compare(password, hashed);
```
**JWT Best Practices**
```python
# BAD - long-lived token, weak secret
token = jwt.encode({"user_id": 1, "exp": datetime.utcnow() + timedelta(days=365)},
"secret123", algorithm="HS256")
# GOOD - short expiry, strong secret, httpOnly cookie delivery
ACCESS_TOKEN_EXPIRY = timedelta(minutes=15)
def create_access_token(user_id: int) -> str:
return jwt.encode(
{"sub": user_id, "exp": datetime.now(timezone.utc) + ACCESS_TOKEN_EXPIRY},
os.environ["JWT_SECRET_KEY"], algorithm="HS256",
)
def set_token_cookie(response: Response, token: str):
response.set_cookie(
key="access_token", value=token,
httponly=True, secure=True, samesite="lax", # not accessible via JS, HTTPS only
max_age=int(ACCESS_TOKEN_EXPIRY.total_seconds()),
)
```
**Session Management Rules**
- Set session timeouts (30 minutes idle, 8 hours absolute)
- Regenerate session ID after login to prevent session fixation
- Store sessions server-side (Redis, database), not in cookies
- Clear sessions on logout (`request.session.clear()`)
- Use `httponly`, `secure`, and `samesite=lax` on session cookies
### 5. Authorization & Access Control
**RBAC Pattern**
```python
# GOOD - role-based access control with decorator
from enum import Enum
class Role(str, Enum):
admin = "admin"
editor = "editor"
viewer = "viewer"
ROLE_HIERARCHY = {Role.admin: 3, Role.editor: 2, Role.viewer: 1}
def require_role(minimum_role: Role):
def decorator(func):
async def wrapper(request: Request, *args, **kwargs):
user = request.state.user
if ROLE_HIERARCHY.get(user.role, 0) < ROLE_HIERARCHY[minimum_role]:
raise HTTPException(status_code=403)
return await func(request, *args, **kwargs)
return wrapper
return decorator
@app.delete("/posts/{post_id}")
@require_role(Role.editor)
async def delete_post(request: Request, post_id: int): ...
```
**Middleware-Based Authorization (Express)**
```typescript
// GOOD - authorization middleware
function requireRole(...allowedRoles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user || !allowedRoles.includes(req.user.role)) {
return res.status(403).json({ error: "Forbidden" });
}
next();
};
}
app.delete("/posts/:id", requireRole("admin", "editor"), deletePostHandler);
```
**Object-Level Permissions**
```python
# BAD - checks auth but not ownership (any user can edit any document)
@app.put("/documents/{doc_id}")
async def update_document(doc_id: int, payload: UpdateDoc, user=Depends(get_current_user)):
doc = await db.get(Document, doc_id)
doc.content = payload.content
# GOOD - verify ownership or admin role on every mutation
@app.put("/documents/{doc_id}")
async def update_document(doc_id: int, payload: UpdateDoc, user=Depends(get_current_user)):
doc = await db.get(Document, doc_id)
if not doc:
raise HTTPException(status_code=404)
if doc.owner_id != user.id and user.role != Role.admin:
raise HTTPException(status_code=403)
doc.content = payload.content
```
### 6. CORS Configuration
**FastAPI**
```python
# BAD - allows everything
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
allow_methods=["*"], allow_headers=["*"])
# GOOD - restrictive CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com", "https://staging.example.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
```
**Express**
```typescript
// BAD
app.use(cors({ origin: true, credentials: true }));
// GOOD - explicit allowlist with callback
const ALLOWED_ORIGINS = ["https://app.example.com"];
app.use(cors({
origin: (origin, cb) => {
if (!origin || ALLOWED_ORIGINS.includes(origin)) cb(null, true);
else cb(new Error("Not allowed by CORS"));
},
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE"],
}));
```
### 7. Security Headers
**Express with Helmet**
```typescript
// GOOD - Helmet sets secure defaults for all critical headers
import helmet from "helmet";
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:"],
frameAncestors: ["'none'"],
},
},
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
}));
```
**FastAPI**
```python
# GOOD - security headers middleware
@app.middleware("http")
async def security_headers(request, call_next):
response = await call_next(request)
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload"
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()"
response.headers["Content-Security-Policy"] = "default-src 'self'; frame-ancestors 'none';"
return response
```
### 8. Secret Management
```python
# BAD - hardcoded secrets
DATABASE_URL = "postgresql://admin:p@ssw0rd@localhost/mydb"
API_KEY = "sk-1234567890abcdef"
JWT_SECRET = "mysecret"
# GOOD - environment variables with validation
import os
def get_required_env(key: str) -> str:
value = os.environ.get(key)
if not value:
raise RuntimeError(f"Required environment variable {key} is not set")
return value
DATABASE_URL = get_required_env("DATABASE_URL")
API_KEY = get_required_env("API_KEY")
JWT_SECRET = get_required_env("JWT_SECRET")
```
**.env and .gitignore**
```bash
# .env (NEVER commit this file)
DATABASE_URL=postgresql://admin:securepass@localhost/mydb
JWT_SECRET=a-very-long-random-string-from-openssl-rand
API_KEY=sk-prod-xxxxxxxxxxxx
```
```gitignore
# .gitignore - always include these
.env
.env.*
!.env.example
*.pem
*.key
credentials.json
```
Commit a `.env.example` with empty values to document required variables without exposing secrets.
### 9. Rate Limiting
**Python (FastAPI with slowapi)**
```python
# GOOD - rate limiting on sensitive endpoints
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
@app.post("/login")
@limiter.limit("5/minute") # brute force protection
async def login(request: Request, credentials: LoginRequest):
...
@app.post("/api/data")
@limiter.limit("100/minute") # general API rate limit
async def get_data(request: Request):
...
```
**Express (express-rate-limit)**
```typescript
// GOOD - tiered rate limiting
import rateLimit from "express-rate-limit";
const generalLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100 });
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5 });
app.use("/api/", generalLimiter);
app.use("/auth/login", authLimiter);
app.use("/auth/register", authLimiter);
```
### 10. Dependency Security
```bash
# Python - audit dependencies
pip install pip-audit
pip-audit # scan for known vulnerabilities
pip-audit --fix # auto-fix where possible
# Node.js - audit dependencies
npm audit # list vulnerabilities
npm audit fix # auto-fix compatible updates
pnpm audit # pnpm equivalent
# Always commit lock files to ensure reproducible builds
# Python: requirements.txt or poetry.lock
# Node.js: package-lock.json, pnpm-lock.yaml, or yarn.lock
```
Run `npm audit --audit-level=high` and `pip-audit --strict` in CI (e.g., GitHub Actions on every PR and weekly schedule). Treat high-severity findings as build failures.
---
## Best Practices
1. **Validate at the boundary, trust nothing inside.** Every piece of user input -- query params, headers, request bodies, file uploads -- must be validated before processing. Use Pydantic or Zod schemas, not manual checks.
2. **Apply the principle of least privilege everywhere.** Default to the most restrictive access. Grant permissions explicitly. Use role-based access control and verify object-level ownership on every mutation.
3. **Never store or log secrets in plain text.** Use environment variables, a secret manager, or encrypted storage. Ensure secrets never appear in logs, error messages, or version control.
4. **Use strong, adaptive password hashing.** Always use argon2 or bcrypt with a sufficient work factor. Never use MD5, SHA-1, or SHA-256 alone for password storage.
5. **Set security headers on every response.** Enable HSTS, CSP, X-Content-Type-Options, X-Frame-Options, and Referrer-Policy. Use Helmet for Express and middleware for FastAPI.
6. **Fail closed, not open.** When authentication or authorization checks encounter errors, deny access by default. Never fall through to an unprotected code path on exception.
7. **Keep dependencies updated and audited.** Run `npm audit` and `pip-audit` in CI pipelines. Pin dependency versions with lock files. Review changelogs before major upgrades.
8. **Enforce rate limiting on all public-facing endpoints.** Apply stricter limits on authentication and password reset endpoints. Use IP-based and account-based limiting together for defense in depth.
---
## Common Pitfalls
- **Trusting user input**: Always validate
- **SQL concatenation**: Use parameters
- **Storing plain passwords**: Use argon2/bcrypt
1. **Trusting client-side validation alone.** Attackers bypass browser validation trivially. Always re-validate on the server.
2. **Using wildcard CORS with credentials.** `allow_origins=["*"]` with credentials is insecure and browsers reject it. Specify exact origins.
3. **Storing JWTs in localStorage.** Any XSS can steal them. Use httpOnly, secure, sameSite cookies instead.
4. **Returning detailed error messages in production.** Stack traces help attackers. Return generic messages to clients, log details server-side.
5. **Using ORM raw query methods unsafely.** `$queryRawUnsafe` and `text()` with f-strings bypass ORM protections. Audit every raw SQL call.
6. **Checking authentication but not authorization.** "Logged in" does not mean "authorized." Check object-level permissions on every write.
7. **Disabling security in dev and shipping it.** CSP, CORS, HTTPS disabled for convenience can reach production. Use environment-aware config.
8. **Ignoring dependency vulnerabilities.** Known CVEs in transitive deps are a top attack vector. Automate auditing in CI.
---
## Security Review Checklist
- [ ] All user input validated with schema (Pydantic / Zod) before processing
- [ ] No string concatenation or interpolation in SQL queries
- [ ] Passwords hashed with argon2 or bcrypt (never MD5/SHA)
- [ ] JWTs have short expiry, use httpOnly cookies, strong secret from env
- [ ] Authorization checked at object level, not just authentication
- [ ] CORS configured with explicit origin allowlist (no wildcards with credentials)
- [ ] Security headers set: CSP, HSTS, X-Content-Type-Options, X-Frame-Options
- [ ] No secrets hardcoded in source -- all from environment variables
- [ ] .env files listed in .gitignore, .env.example committed
- [ ] Rate limiting applied to login, registration, and password reset endpoints
- [ ] File uploads validated by MIME type, size, and sanitized filename
- [ ] Error responses do not leak stack traces or internal details
- [ ] Dependencies audited with npm audit / pip-audit (no high-severity CVEs)
- [ ] HTTPS enforced in production with HSTS preload
- [ ] No use of eval(), dangerouslySetInnerHTML (without DOMPurify), or innerHTML
---
## 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
@@ -0,0 +1,193 @@
# OWASP Top 10 (2021) Cheat Sheet
Quick reference for the OWASP Top 10 web application security risks.
---
## A01: Broken Access Control
**Risk**: Users act outside intended permissions (view other users' data, modify access).
**Prevention**: Deny by default. Enforce ownership. Disable directory listing. Log failures.
```python
# Enforce ownership check
def get_order(order_id, current_user):
order = db.query(Order).get(order_id)
if order.user_id != current_user.id:
raise PermissionError("Access denied")
return order
```
## A02: Cryptographic Failures
**Risk**: Exposure of sensitive data due to weak or missing encryption.
**Prevention**: Encrypt data at rest and in transit. Use strong algorithms (AES-256, bcrypt). Never store plaintext passwords.
```python
from passlib.hash import bcrypt
hashed = bcrypt.hash(password)
assert bcrypt.verify(password, hashed)
```
## A03: Injection
**Risk**: Untrusted data sent to an interpreter as part of a command or query.
**Prevention**: Use parameterized queries. Validate and sanitize all input. Use ORMs.
```python
# WRONG: cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
# RIGHT:
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
```
```typescript
// WRONG: db.query(`SELECT * FROM users WHERE id = ${id}`)
// RIGHT:
db.query("SELECT * FROM users WHERE id = $1", [id]);
```
## A04: Insecure Design
**Risk**: Missing or ineffective security controls due to flawed architecture.
**Prevention**: Use threat modeling. Apply secure design patterns. Establish reference architectures. Write abuse-case tests.
```python
# Rate-limit sensitive operations
from functools import lru_cache
from datetime import datetime, timedelta
LOGIN_ATTEMPTS = {} # Use Redis in production
def check_rate_limit(ip: str, max_attempts=5, window=300):
now = datetime.now().timestamp()
attempts = [t for t in LOGIN_ATTEMPTS.get(ip, []) if now - t < window]
if len(attempts) >= max_attempts:
raise RateLimitExceeded()
attempts.append(now)
LOGIN_ATTEMPTS[ip] = attempts
```
## A05: Security Misconfiguration
**Risk**: Default configs, incomplete setups, open cloud storage, verbose errors.
**Prevention**: Repeatable hardening process. Minimal platform. Remove unused features. Review cloud permissions.
```yaml
# Docker: don't run as root
FROM python:3.12-slim
RUN useradd -m appuser
USER appuser
```
## A06: Vulnerable and Outdated Components
**Risk**: Using components with known vulnerabilities.
**Prevention**: Remove unused dependencies. Monitor CVEs. Use `pip audit`, `npm audit`. Pin versions.
```bash
pip audit # Python
npm audit # Node.js
npx depcheck # Find unused deps
```
## A07: Identification and Authentication Failures
**Risk**: Weak authentication, credential stuffing, session fixation.
**Prevention**: MFA. Strong password policies. Secure session management. Throttle failed logins.
```python
# Secure session config (Flask)
app.config.update(
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE="Lax",
PERMANENT_SESSION_LIFETIME=timedelta(hours=1),
)
```
## A08: Software and Data Integrity Failures
**Risk**: Code and infrastructure that does not protect against integrity violations (CI/CD, unsigned updates).
**Prevention**: Verify signatures. Use lock files. Review CI/CD pipelines. Use Subresource Integrity.
```html
<!-- Subresource Integrity -->
<script src="https://cdn.example.com/lib.js"
integrity="sha384-abc123..."
crossorigin="anonymous"></script>
```
## A09: Security Logging and Monitoring Failures
**Risk**: Insufficient logging makes breaches undetectable.
**Prevention**: Log auth events, access control failures, input validation failures. Set up alerts.
```python
import logging
logger = logging.getLogger("security")
def login(username, password):
user = authenticate(username, password)
if not user:
logger.warning("Failed login attempt", extra={
"username": username,
"ip": request.remote_addr,
"timestamp": datetime.utcnow().isoformat(),
})
raise AuthenticationError()
logger.info("Successful login", extra={"user_id": user.id})
```
## A10: Server-Side Request Forgery (SSRF)
**Risk**: Application fetches remote resources without validating user-supplied URLs.
**Prevention**: Allowlist URLs/domains. Block private IP ranges. Disable redirects.
```python
from urllib.parse import urlparse
import ipaddress
ALLOWED_HOSTS = {"api.example.com", "cdn.example.com"}
def validate_url(url: str) -> bool:
parsed = urlparse(url)
if parsed.hostname not in ALLOWED_HOSTS:
return False
try:
ip = ipaddress.ip_address(parsed.hostname)
if ip.is_private or ip.is_loopback:
return False
except ValueError:
pass # hostname, not IP — already checked against allowlist
return True
```
---
## Quick Reference Table
| ID | Name | Key Control |
|-----|-------------------------------|--------------------------------|
| A01 | Broken Access Control | Deny by default, enforce ownership |
| A02 | Cryptographic Failures | Encrypt in transit + at rest |
| A03 | Injection | Parameterized queries |
| A04 | Insecure Design | Threat modeling, abuse cases |
| A05 | Security Misconfiguration | Hardened defaults, minimal surface |
| A06 | Vulnerable Components | Audit deps, pin versions |
| A07 | Auth Failures | MFA, session security |
| A08 | Integrity Failures | Verify signatures, lock files |
| A09 | Logging Failures | Log security events, alert |
| A10 | SSRF | Allowlist URLs, block private IPs |
*Source: [OWASP Top 10 (2021)](https://owasp.org/Top10/)*
@@ -0,0 +1,217 @@
# Security Headers Reference
Comprehensive reference for HTTP security headers with recommended values and implementation examples.
---
## Header Reference Table
| Header | Purpose | Recommended Value |
|--------|---------|-------------------|
| `Content-Security-Policy` | Prevent XSS, data injection | See detailed section below |
| `Strict-Transport-Security` | Force HTTPS | `max-age=63072000; includeSubDomains; preload` |
| `X-Frame-Options` | Prevent clickjacking | `DENY` or `SAMEORIGIN` |
| `X-Content-Type-Options` | Prevent MIME sniffing | `nosniff` |
| `Referrer-Policy` | Control referer leakage | `strict-origin-when-cross-origin` |
| `Permissions-Policy` | Restrict browser features | See detailed section below |
---
## Content-Security-Policy (CSP)
Controls which resources the browser is allowed to load.
**Starter policy (strict):**
```
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
```
**Key directives:**
| Directive | Controls | Example |
|-----------|----------|---------|
| `default-src` | Fallback for all resource types | `'self'` |
| `script-src` | JavaScript sources | `'self' https://cdn.example.com` |
| `style-src` | CSS sources | `'self' 'unsafe-inline'` |
| `img-src` | Image sources | `'self' data: https:` |
| `connect-src` | Fetch, XHR, WebSocket targets | `'self' https://api.example.com` |
| `frame-ancestors` | Who can embed this page | `'none'` |
| `form-action` | Form submission targets | `'self'` |
## Strict-Transport-Security (HSTS)
Forces browsers to use HTTPS for all future requests to this domain.
```
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
```
- `max-age=63072000` — 2 years (minimum for preload list)
- `includeSubDomains` — apply to all subdomains
- `preload` — opt into browser preload lists
## X-Frame-Options
Prevents the page from being embedded in iframes (clickjacking protection).
```
X-Frame-Options: DENY
```
| Value | Behavior |
|-------|----------|
| `DENY` | Never allow framing |
| `SAMEORIGIN` | Allow framing by same origin only |
Note: `frame-ancestors` in CSP is the modern replacement but set both for backward compatibility.
## X-Content-Type-Options
Prevents browsers from MIME-sniffing the response content type.
```
X-Content-Type-Options: nosniff
```
Always pair with correct `Content-Type` headers on responses.
## Referrer-Policy
Controls how much referrer information is sent with requests.
```
Referrer-Policy: strict-origin-when-cross-origin
```
| Value | Cross-Origin Sends | Same-Origin Sends |
|-------|-------------------|-------------------|
| `no-referrer` | Nothing | Nothing |
| `origin` | Origin only | Origin only |
| `strict-origin-when-cross-origin` | Origin (HTTPS only) | Full URL |
| `same-origin` | Nothing | Full URL |
## Permissions-Policy
Restricts which browser features the page can use.
```
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
```
| Feature | Recommended | Description |
|---------|-------------|-------------|
| `camera` | `()` | Disable camera access |
| `microphone` | `()` | Disable microphone |
| `geolocation` | `()` | Disable location |
| `payment` | `()` | Disable Payment API |
| `usb` | `()` | Disable USB access |
| `fullscreen` | `(self)` | Allow fullscreen for same origin |
---
## Implementation: Python (FastAPI)
```python
from fastapi import FastAPI
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
app = FastAPI()
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next) -> Response:
response = await call_next(request)
response.headers["Content-Security-Policy"] = (
"default-src 'self'; script-src 'self'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data: https:; "
"frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
)
response.headers["Strict-Transport-Security"] = (
"max-age=63072000; includeSubDomains; preload"
)
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Permissions-Policy"] = (
"camera=(), microphone=(), geolocation=(), payment=()"
)
return response
app.add_middleware(SecurityHeadersMiddleware)
```
## Implementation: Node.js (Express)
```typescript
import helmet from "helmet";
import express from "express";
const app = express();
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
},
},
strictTransportSecurity: {
maxAge: 63072000,
includeSubDomains: true,
preload: true,
},
frameguard: { action: "deny" },
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
permissionsPolicy: {
features: {
camera: [],
microphone: [],
geolocation: [],
payment: [],
},
},
})
);
```
## Implementation: Next.js
```typescript
// next.config.ts
const securityHeaders = [
{ key: "Content-Security-Policy", value: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'none'" },
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=(), payment=()" },
];
export default {
async headers() {
return [{ source: "/(.*)", headers: securityHeaders }];
},
};
```
---
## Verification
```bash
# Check headers on a live site
curl -I https://example.com
# Use securityheaders.com for a grade
# https://securityheaders.com/?q=https://example.com
```
*Source: [MDN HTTP Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers), [OWASP Secure Headers](https://owasp.org/www-project-secure-headers/)*
@@ -0,0 +1,200 @@
#!/usr/bin/env python3
"""Security audit scanner for common vulnerabilities.
Scans source files for hardcoded secrets, eval() usage, SQL string
concatenation, and sensitive data in console output. Outputs JSON.
Usage:
python security-audit.py ./src
python security-audit.py ./src --severity high --format pretty
"""
import argparse
import json
import os
import re
import sys
from dataclasses import asdict, dataclass, field
from pathlib import Path
SCAN_EXTENSIONS = {
".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".go",
".rb", ".php", ".env", ".yaml", ".yml", ".toml", ".json",
}
SKIP_DIRS = {
"node_modules", ".git", "__pycache__", ".venv", "venv",
"dist", "build", ".next", ".nuxt", "vendor",
}
@dataclass
class Finding:
file: str
line: int
rule: str
severity: str
message: str
snippet: str
@dataclass
class AuditReport:
scanned_files: int = 0
findings: list = field(default_factory=list)
summary: dict = field(default_factory=dict)
# --- Detection Rules ---
SECRET_PATTERNS = [
(r'(?i)(api[_-]?key|apikey)\s*[=:]\s*["\'][A-Za-z0-9_\-]{16,}["\']', "Possible API key"),
(r'(?i)(secret|password|passwd|pwd)\s*[=:]\s*["\'][^"\']{8,}["\']', "Possible hardcoded secret"),
(r'(?i)(aws_access_key_id|aws_secret_access_key)\s*[=:]\s*["\'][^"\']+["\']', "AWS credential"),
(r'(?i)bearer\s+[A-Za-z0-9_\-\.]{20,}', "Possible bearer token"),
(r'(?i)(ghp_|gho_|github_pat_)[A-Za-z0-9_]{20,}', "GitHub token"),
(r'(?i)(sk-|pk_live_|pk_test_|sk_live_|sk_test_)[A-Za-z0-9]{20,}', "API secret key"),
(r'-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----', "Private key in source"),
]
EVAL_PATTERNS = [
(r'\beval\s*\(', "eval() usage detected"),
(r'\bexec\s*\(', "exec() usage detected (Python)"),
(r'new\s+Function\s*\(', "new Function() usage (dynamic code)"),
(r'\bchild_process\.exec\s*\(', "child_process.exec (command injection risk)"),
(r'subprocess\.call\s*\([^)]*shell\s*=\s*True', "subprocess with shell=True"),
(r'os\.system\s*\(', "os.system() usage (command injection risk)"),
]
SQL_PATTERNS = [
(r'(?i)(SELECT|INSERT|UPDATE|DELETE|DROP)\s+.*([\+]|\.format\(|f["\']|%\s)', "SQL string concatenation"),
(r'(?i)execute\s*\(\s*f["\']', "SQL f-string in execute()"),
(r'(?i)\.query\s*\(\s*`[^`]*\$\{', "SQL template literal injection"),
(r'(?i)\.raw\s*\(\s*f["\']', "Raw SQL with f-string"),
]
SENSITIVE_LOG_PATTERNS = [
(r'console\.log\s*\(.*(?i)(password|secret|token|key|credential)', "Sensitive data in console.log"),
(r'print\s*\(.*(?i)(password|secret|token|key|credential)', "Sensitive data in print()"),
(r'logger?\.(info|debug|warn)\s*\(.*(?i)(password|secret|token)', "Sensitive data in logger"),
]
RULES = [
("hardcoded-secret", "high", SECRET_PATTERNS),
("dangerous-eval", "high", EVAL_PATTERNS),
("sql-injection", "high", SQL_PATTERNS),
("sensitive-logging", "medium", SENSITIVE_LOG_PATTERNS),
]
def should_scan(path: Path) -> bool:
if path.suffix not in SCAN_EXTENSIONS:
return False
for part in path.parts:
if part in SKIP_DIRS:
return False
return True
def scan_file(filepath: Path) -> list[Finding]:
findings = []
try:
content = filepath.read_text(encoding="utf-8", errors="ignore")
except (OSError, PermissionError):
return findings
lines = content.splitlines()
for line_num, line in enumerate(lines, start=1):
stripped = line.strip()
if stripped.startswith(("#", "//", "*", "/*")):
continue
for rule_name, severity, patterns in RULES:
for pattern, message in patterns:
if re.search(pattern, line):
findings.append(Finding(
file=str(filepath),
line=line_num,
rule=rule_name,
severity=severity,
message=message,
snippet=line.strip()[:120],
))
return findings
def scan_directory(target: Path, severity_filter: str | None = None) -> AuditReport:
report = AuditReport()
severity_order = {"high": 3, "medium": 2, "low": 1}
min_severity = severity_order.get(severity_filter, 0) if severity_filter else 0
for root, dirs, files in os.walk(target):
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
for fname in files:
fpath = Path(root) / fname
if not should_scan(fpath):
continue
report.scanned_files += 1
for finding in scan_file(fpath):
if severity_order.get(finding.severity, 0) >= min_severity:
report.findings.append(finding)
report.summary = {
"total": len(report.findings),
"high": sum(1 for f in report.findings if f.severity == "high"),
"medium": sum(1 for f in report.findings if f.severity == "medium"),
"low": sum(1 for f in report.findings if f.severity == "low"),
"by_rule": {},
}
for f in report.findings:
report.summary["by_rule"][f.rule] = report.summary["by_rule"].get(f.rule, 0) + 1
return report
def main():
parser = argparse.ArgumentParser(
description="Scan source files for common security issues.",
epilog="Example: python security-audit.py ./src --severity high",
)
parser.add_argument("target", help="Directory or file to scan")
parser.add_argument(
"--severity", choices=["low", "medium", "high"],
help="Minimum severity to report (default: all)",
)
parser.add_argument(
"--format", choices=["json", "pretty"], default="json",
help="Output format (default: json)",
)
args = parser.parse_args()
target = Path(args.target)
if not target.exists():
print(f"Error: {target} does not exist", file=sys.stderr)
sys.exit(1)
report = scan_directory(target, args.severity)
output = {
"scanned_files": report.scanned_files,
"summary": report.summary,
"findings": [asdict(f) for f in report.findings],
}
if args.format == "pretty":
print(f"\nScanned {report.scanned_files} files\n")
print(f"Findings: {report.summary['total']} total "
f"({report.summary['high']} high, {report.summary['medium']} medium)")
print("-" * 60)
for f in report.findings:
print(f"[{f.severity.upper()}] {f.file}:{f.line}")
print(f" Rule: {f.rule}")
print(f" {f.message}")
print(f" > {f.snippet}")
print()
else:
print(json.dumps(output, indent=2))
sys.exit(1 if report.summary.get("high", 0) > 0 else 0)
if __name__ == "__main__":
main()
@@ -0,0 +1,120 @@
# Security Code Review Checklist
**Project**: _______________
**Reviewer**: _______________
**Date**: _______________
**Scope**: _______________
---
## Authentication and Session Management
- [ ] Passwords hashed with bcrypt/argon2 (not MD5/SHA1)
- [ ] Session tokens are cryptographically random
- [ ] Session cookies use `Secure`, `HttpOnly`, `SameSite` flags
- [ ] Session timeout is enforced (idle and absolute)
- [ ] Failed login attempts are rate-limited
- [ ] MFA is available for sensitive accounts
- [ ] Password reset tokens expire and are single-use
## Authorization and Access Control
- [ ] Access denied by default (allowlist approach)
- [ ] Server-side authorization on every request
- [ ] Resource ownership verified before access
- [ ] Role/permission checks cannot be bypassed via direct URL
- [ ] Admin endpoints have separate authentication
- [ ] CORS policy restricts allowed origins
## Input Validation
- [ ] All user input validated server-side
- [ ] Parameterized queries used for all database access
- [ ] No string concatenation in SQL/commands
- [ ] File uploads validated (type, size, content)
- [ ] Path traversal prevented on file operations
- [ ] JSON/XML parsers configured against XXE
## Output Encoding
- [ ] HTML output properly escaped (XSS prevention)
- [ ] Content-Type headers set correctly on all responses
- [ ] API responses do not leak stack traces in production
- [ ] Error messages do not reveal system internals
- [ ] Sensitive data excluded from logs
## Cryptography
- [ ] TLS 1.2+ enforced for all connections
- [ ] Sensitive data encrypted at rest
- [ ] No hardcoded secrets, keys, or passwords in source
- [ ] Secrets loaded from environment variables or vault
- [ ] Strong algorithms used (AES-256, RSA-2048+, SHA-256+)
- [ ] No custom cryptographic implementations
## Security Headers
- [ ] Content-Security-Policy configured
- [ ] Strict-Transport-Security enabled
- [ ] X-Frame-Options set to DENY
- [ ] X-Content-Type-Options set to nosniff
- [ ] Referrer-Policy configured
- [ ] Permissions-Policy restricts unused features
## Dependencies
- [ ] No known vulnerabilities (`npm audit` / `pip audit` clean)
- [ ] Unused dependencies removed
- [ ] Dependencies pinned to specific versions
- [ ] Lock file committed and up to date
## Logging and Monitoring
- [ ] Authentication events logged (success and failure)
- [ ] Authorization failures logged
- [ ] Sensitive data not written to logs
- [ ] Log injection prevented (user input sanitized in logs)
- [ ] Alerts configured for suspicious patterns
## API Security
- [ ] Rate limiting on all public endpoints
- [ ] Request size limits configured
- [ ] API keys/tokens not exposed in URLs
- [ ] Pagination enforced on list endpoints
- [ ] HTTPS required (HTTP redirects or blocks)
## Infrastructure
- [ ] Debug mode disabled in production
- [ ] Default credentials changed
- [ ] Unnecessary ports/services disabled
- [ ] Container runs as non-root user
- [ ] Environment variables not logged at startup
---
## Summary
| Category | Pass | Fail | N/A |
|----------|------|------|-----|
| Authentication | | | |
| Authorization | | | |
| Input Validation | | | |
| Output Encoding | | | |
| Cryptography | | | |
| Security Headers | | | |
| Dependencies | | | |
| Logging | | | |
| API Security | | | |
| Infrastructure | | | |
**Overall Assessment**: [ ] Pass / [ ] Conditional Pass / [ ] Fail
**Notes**:
**Follow-up Actions**:
+649 -50
View File
@@ -1,90 +1,689 @@
---
name: pytest
description: >
Trigger this skill whenever writing, debugging, or refactoring Python tests, or when pytest fixtures, parametrization, mocking, or coverage are mentioned. Activate for any .py test file, test_* function, conftest.py, pytest.ini, or pyproject.toml [tool.pytest] reference. Also use when the user asks about Python test patterns, test organization, or test-driven development in a Python context.
---
# pytest
## Description
Python testing with pytest including fixtures, parametrization, and mocking.
## When to Use
- Writing Python tests
- Test fixtures and setup
- Mocking dependencies
## When NOT to Use
- JavaScript or TypeScript testing -- use the `testing/vitest` skill instead
- Projects that explicitly mandate unittest-only by convention with no pytest dependency
- Non-Python test files or environments
---
## Core Patterns
### Basic Tests
### 1. Fixtures
Fixtures provide reusable setup and teardown logic. They are requested by name as test function parameters.
#### Function-Scoped Fixtures (default)
A new instance is created for every test that requests it.
```python
import pytest
from myapp.models import User
from myapp.db import Session
def test_addition():
assert 1 + 1 == 2
def test_exception():
with pytest.raises(ValueError, match="Invalid"):
raise ValueError("Invalid input")
```
### Fixtures
```python
@pytest.fixture
def user():
return User(id=1, name="Test")
"""Fresh user instance per test."""
return User(id=1, name="Alice", email="alice@example.com")
@pytest.fixture
def db_session():
session = create_session()
yield session
session.close()
def test_with_fixtures(user, db_session):
db_session.add(user)
assert user.id is not None
def test_user_display_name(user):
assert user.display_name() == "Alice"
def test_user_email_domain(user):
assert user.email_domain() == "example.com"
```
### Parametrization
#### Class and Module Scope
Use broader scopes for expensive resources that are safe to share.
```python
@pytest.mark.parametrize("input,expected", [
@pytest.fixture(scope="class")
def api_client():
"""Shared across all tests in a test class."""
client = APIClient(base_url="http://testserver")
client.authenticate(token="test-token")
return client
@pytest.fixture(scope="module")
def database_schema():
"""Created once per test module, shared across all tests in the file."""
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
yield engine
engine.dispose()
@pytest.fixture(scope="session")
def redis_connection():
"""Created once for the entire test session."""
conn = Redis(host="localhost", port=6379, db=15)
conn.flushdb()
yield conn
conn.flushdb()
conn.close()
```
#### Yield Fixtures for Teardown
`yield` separates setup from teardown. Code after `yield` runs after the test completes, even if the test fails.
```python
@pytest.fixture
def db_session():
session = Session()
session.begin()
yield session
session.rollback()
session.close()
@pytest.fixture
def temp_config(tmp_path):
config_file = tmp_path / "config.yaml"
config_file.write_text("debug: true\nlog_level: INFO\n")
yield config_file
# tmp_path is automatically cleaned up by pytest
```
#### Autouse Fixtures
Apply a fixture to every test automatically without requesting it by name.
```python
@pytest.fixture(autouse=True)
def reset_environment(monkeypatch):
"""Ensure each test starts with clean environment variables."""
monkeypatch.delenv("API_KEY", raising=False)
monkeypatch.delenv("DATABASE_URL", raising=False)
@pytest.fixture(autouse=True)
def freeze_time():
"""Pin time for deterministic tests."""
with freeze_time("2025-06-15T12:00:00Z"):
yield
```
#### Factory Fixtures
Return a factory function when tests need multiple instances with varying parameters.
```python
@pytest.fixture
def make_user():
"""Factory that creates users with sensible defaults."""
created = []
def _make_user(name="Test User", role="viewer", active=True):
user = User(name=name, role=role, active=active)
created.append(user)
return user
yield _make_user
# Teardown: clean up all created users
for u in created:
u.delete()
def test_admin_permissions(make_user):
admin = make_user(name="Admin", role="admin")
viewer = make_user(name="Viewer", role="viewer")
assert admin.can_delete_users() is True
assert viewer.can_delete_users() is False
```
#### Parametrized Fixtures with request.param
Run the same test against multiple fixture variants.
```python
@pytest.fixture(params=["sqlite", "postgresql"])
def db_engine(request):
"""Test against multiple database backends."""
if request.param == "sqlite":
engine = create_engine("sqlite:///:memory:")
elif request.param == "postgresql":
engine = create_engine("postgresql://test:test@localhost/testdb")
yield engine
engine.dispose()
def test_insert_and_query(db_engine):
# This test runs twice: once with sqlite, once with postgresql
with db_engine.connect() as conn:
conn.execute(text("CREATE TABLE t (id INT)"))
conn.execute(text("INSERT INTO t VALUES (1)"))
result = conn.execute(text("SELECT * FROM t")).fetchall()
assert len(result) == 1
```
---
### 2. Parametrize
#### Single Parameter
```python
@pytest.mark.parametrize("email", [
"user@example.com",
"admin@test.org",
"name+tag@domain.co.uk",
])
def test_valid_email_accepted(email):
assert is_valid_email(email) is True
```
#### Multiple Parameters
```python
@pytest.mark.parametrize("input_text, expected", [
("hello", "HELLO"),
("world", "WORLD"),
("", ""),
("already UPPER", "ALREADY UPPER"),
])
def test_uppercase(input, expected):
assert input.upper() == expected
def test_uppercase(input_text, expected):
assert input_text.upper() == expected
```
### Mocking
#### Custom IDs for Readable Output
```python
from unittest.mock import Mock, patch
def test_with_mock():
service = Mock()
service.get_user.return_value = {"id": 1}
result = service.get_user(1)
assert result["id"] == 1
@patch('module.external_api')
def test_with_patch(mock_api):
mock_api.fetch.return_value = {"data": []}
# Test code that uses external_api
@pytest.mark.parametrize("status_code, should_retry", [
pytest.param(200, False, id="success-no-retry"),
pytest.param(429, True, id="rate-limited-retry"),
pytest.param(500, True, id="server-error-retry"),
pytest.param(404, False, id="not-found-no-retry"),
])
def test_retry_logic(status_code, should_retry):
response = MockResponse(status_code=status_code)
assert should_retry_request(response) is should_retry
```
#### Indirect Parametrize
Pass parameters through a fixture rather than directly to the test.
```python
@pytest.fixture
def user_role(request):
"""Create a user with the given role."""
return User(name="Test", role=request.param)
@pytest.mark.parametrize("user_role", ["admin", "editor", "viewer"], indirect=True)
def test_dashboard_access(user_role):
if user_role.role == "admin":
assert user_role.can_access("/admin/dashboard") is True
else:
assert user_role.can_access("/admin/dashboard") is False
```
#### Stacking Parametrize Decorators
Creates the cartesian product of all parameter sets.
```python
@pytest.mark.parametrize("method", ["GET", "POST", "PUT", "DELETE"])
@pytest.mark.parametrize("auth", ["token", "session", "none"])
def test_endpoint_auth(method, auth):
# Runs 4 x 3 = 12 test cases
response = make_request(method=method, auth_type=auth)
if auth == "none":
assert response.status_code == 401
else:
assert response.status_code in (200, 201, 204)
```
---
### 3. Mocking
#### monkeypatch -- Environment Variables and Attributes
```python
def test_reads_api_key_from_env(monkeypatch):
monkeypatch.setenv("API_KEY", "test-key-12345")
config = load_config()
assert config.api_key == "test-key-12345"
def test_missing_api_key_raises(monkeypatch):
monkeypatch.delenv("API_KEY", raising=False)
with pytest.raises(ConfigError, match="API_KEY is required"):
load_config()
def test_override_attribute(monkeypatch):
monkeypatch.setattr("myapp.settings.MAX_RETRIES", 0)
assert retry_request(failing_url) is None # No retries attempted
def test_override_dict_item(monkeypatch):
monkeypatch.setitem(app_config, "timeout", 1)
assert app_config["timeout"] == 1
```
#### unittest.mock.patch
```python
from unittest.mock import patch, Mock, AsyncMock
@patch("myapp.services.payment.stripe.Charge.create")
def test_charge_customer(mock_charge):
mock_charge.return_value = Mock(id="ch_123", status="succeeded")
result = process_payment(amount=1000, currency="usd", token="tok_visa")
mock_charge.assert_called_once_with(
amount=1000, currency="usd", source="tok_visa"
)
assert result.charge_id == "ch_123"
@patch("myapp.services.email.send_email")
@patch("myapp.services.user.UserRepository.find_by_id")
def test_send_welcome_email(mock_find, mock_send):
mock_find.return_value = User(id=1, email="new@example.com")
mock_send.return_value = True
send_welcome(user_id=1)
mock_send.assert_called_once_with(
to="new@example.com", template="welcome"
)
```
#### responses Library for HTTP Mocking
```python
import responses
import requests
@responses.activate
def test_fetch_user_from_api():
responses.add(
responses.GET,
"https://api.example.com/users/1",
json={"id": 1, "name": "Alice"},
status=200,
)
result = fetch_user(user_id=1)
assert result["name"] == "Alice"
assert len(responses.calls) == 1
assert responses.calls[0].request.url == "https://api.example.com/users/1"
@responses.activate
def test_api_timeout_handling():
responses.add(
responses.GET,
"https://api.example.com/users/1",
body=requests.exceptions.ConnectionError("Connection timed out"),
)
with pytest.raises(ServiceUnavailableError):
fetch_user(user_id=1)
```
#### pytest-mock's mocker Fixture
```python
def test_with_mocker(mocker):
mock_repo = mocker.patch("myapp.services.OrderRepository")
mock_repo.return_value.get_by_id.return_value = Order(
id=1, status="pending"
)
service = OrderService()
order = service.get_order(1)
assert order.status == "pending"
mock_repo.return_value.get_by_id.assert_called_once_with(1)
def test_spy_on_method(mocker):
spy = mocker.spy(UserService, "validate_email")
service = UserService()
service.register("alice@example.com")
spy.assert_called_once_with(service, "alice@example.com")
```
---
### 4. Async Testing
#### pytest-asyncio Basics
```python
import pytest
import httpx
@pytest.mark.asyncio
async def test_async_fetch():
async with httpx.AsyncClient() as client:
response = await client.get("https://httpbin.org/get")
assert response.status_code == 200
@pytest.mark.asyncio
async def test_async_exception():
with pytest.raises(ValueError, match="invalid"):
await validate_async_input("")
```
#### Async Fixtures
```python
@pytest.fixture
async def async_db_session():
session = AsyncSession(bind=async_engine)
await session.begin()
yield session
await session.rollback()
await session.close()
@pytest.mark.asyncio
async def test_async_query(async_db_session):
result = await async_db_session.execute(
select(User).where(User.active == True)
)
users = result.scalars().all()
assert len(users) >= 0
```
#### Configuring asyncio Mode
In `pyproject.toml` or `pytest.ini`, set the default mode to avoid repeating the marker:
```toml
# pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
```
With `asyncio_mode = "auto"`, any `async def test_*` function is automatically treated as async -- no `@pytest.mark.asyncio` needed.
---
### 5. Test Organization
#### conftest.py Hierarchy
```
tests/
├── conftest.py # Session/global fixtures (db connection, app client)
├── unit/
│ ├── conftest.py # Unit-specific fixtures (mocked services)
│ ├── test_models.py
│ └── test_utils.py
├── integration/
│ ├── conftest.py # Integration fixtures (real db session, test server)
│ ├── test_api.py
│ └── test_repositories.py
└── e2e/
├── conftest.py # E2E fixtures (browser, full app)
└── test_workflows.py
```
Fixtures in a `conftest.py` are available to all tests in the same directory and below. No imports needed.
#### Test Discovery
pytest discovers tests by default based on these rules:
- Files matching `test_*.py` or `*_test.py`
- Classes prefixed with `Test` (no `__init__` method)
- Functions prefixed with `test_`
Configure custom discovery in `pyproject.toml`:
```toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
```
#### Markers
```python
import pytest
import sys
# Built-in markers
@pytest.mark.skip(reason="Not implemented yet")
def test_future_feature():
pass
@pytest.mark.skipif(
sys.platform == "win32", reason="Unix-only functionality"
)
def test_unix_permissions():
pass
@pytest.mark.xfail(reason="Known bug #1234, fix pending")
def test_known_broken():
result = buggy_function()
assert result == "expected"
```
#### Custom Markers
Register markers in `pyproject.toml` to avoid warnings:
```toml
[tool.pytest.ini_options]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks integration tests requiring external services",
"smoke: critical path tests for quick validation",
]
```
```python
@pytest.mark.slow
def test_full_data_migration():
migrate_all_records() # Takes 30+ seconds
assert count_records() == EXPECTED_TOTAL
@pytest.mark.smoke
def test_health_endpoint(client):
response = client.get("/health")
assert response.status_code == 200
```
Run selectively:
```bash
pytest -m "smoke" # Only smoke tests
pytest -m "not slow" # Skip slow tests
pytest -m "integration and not slow" # Integration but not slow
```
---
### 6. Coverage
#### Basic Usage
```bash
pytest --cov=src --cov-report=term-missing
pytest --cov=src --cov-report=html # Generates htmlcov/
pytest --cov=src --cov-branch # Enable branch coverage
```
#### Configuration in pyproject.toml
```toml
[tool.pytest.ini_options]
addopts = "--cov=src --cov-report=term-missing --cov-fail-under=80"
[tool.coverage.run]
source = ["src"]
branch = true
omit = [
"*/migrations/*",
"*/tests/*",
"*/__pycache__/*",
"*/conftest.py",
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if TYPE_CHECKING:",
"raise NotImplementedError",
"@overload",
]
fail_under = 80
show_missing = true
```
#### .coveragerc Alternative
If not using `pyproject.toml`, create `.coveragerc`:
```ini
[run]
source = src
branch = true
[report]
fail_under = 80
show_missing = true
exclude_lines =
pragma: no cover
def __repr__
if TYPE_CHECKING:
```
---
### 7. Assertions
#### pytest.raises for Exceptions
```python
def test_raises_value_error():
with pytest.raises(ValueError) as exc_info:
parse_age("not-a-number")
assert "invalid literal" in str(exc_info.value)
def test_raises_with_match():
with pytest.raises(PermissionError, match=r"User .+ lacks role 'admin'"):
authorize(user=viewer, required_role="admin")
```
#### pytest.approx for Floating Point
```python
def test_circle_area():
assert calculate_area(radius=5) == pytest.approx(78.5398, rel=1e-4)
def test_approx_list():
result = distribute_evenly(total=100, buckets=3)
assert result == pytest.approx([33.33, 33.33, 33.34], abs=0.01)
```
#### Custom Assertion Helpers
Build reusable assertion logic for domain-specific validation.
```python
def assert_valid_api_response(response, expected_status=200):
"""Reusable assertion for API responses."""
assert response.status_code == expected_status, (
f"Expected {expected_status}, got {response.status_code}: "
f"{response.text}"
)
data = response.json()
assert "error" not in data, f"Unexpected error: {data['error']}"
return data
def test_create_user(client):
response = client.post("/users", json={"name": "Alice"})
data = assert_valid_api_response(response, expected_status=201)
assert data["name"] == "Alice"
assert "id" in data
```
---
## Best Practices
1. Use fixtures for test setup
2. Parametrize for multiple test cases
3. Mock external dependencies
4. Use descriptive test names
5. Keep tests independent
1. **Name tests descriptively** -- Use `test_[function]_[scenario]_[expected]` so failures are self-explanatory without reading the test body. `test_parse_date_invalid_format_raises_valueerror` tells you everything.
2. **Keep tests independent** -- Never rely on test execution order. Each test should set up its own state via fixtures and tear it down afterward. Shared mutable state between tests is the top cause of flaky suites.
3. **One assertion focus per test** -- A test can have multiple `assert` statements, but they should all verify the same behavior. If you need to check two independent behaviors, write two tests.
4. **Use fixtures over setup methods** -- Prefer composable fixtures over `setUp`/`tearDown` methods or `setup_function`. Fixtures are explicit about dependencies, reusable across files via `conftest.py`, and support scoping.
5. **Mock at the boundary, not in the middle** -- Mock external services, databases, and network calls. Do not mock internal functions unless they are truly expensive. Over-mocking produces tests that pass but verify nothing.
6. **Use `tmp_path` for file operations** -- pytest's built-in `tmp_path` fixture provides a unique temporary directory per test. Never write to the real filesystem in tests.
7. **Pin randomness and time** -- When testing code that depends on randomness or the current time, use `random.seed()` or a time-freezing library to make tests deterministic.
8. **Run the full suite in CI with branch coverage** -- Local development can use `pytest -x` for fast feedback (stop on first failure), but CI must run the full suite with `--cov-branch` to catch untested branches and regressions.
---
## Common Pitfalls
- **Shared state**: Use fresh fixtures
- **Over-mocking**: Only mock boundaries
- **Slow tests**: Use markers for slow tests
1. **Shared mutable fixtures** -- A module-scoped fixture returning a mutable object (list, dict, instance) gets modified by one test and breaks another. Return fresh copies or use function scope for mutable data.
2. **Patching the wrong import path** -- `@patch("myapp.services.requests.get")` patches where `requests.get` is looked up, not where it is defined. If `services.py` does `from requests import get`, you must patch `myapp.services.get`, not `requests.get`.
3. **Forgetting to await in async tests** -- Omitting `await` makes the test pass vacuously because it never actually runs the coroutine. Always `await` the function under test and use `@pytest.mark.asyncio`.
4. **Tests that depend on execution order** -- If test B relies on side effects from test A, parallel test execution (pytest-xdist) and `--randomly` will expose the coupling immediately. Fix by making each test self-contained.
5. **Asserting on mock call count without checking arguments** -- `mock.assert_called_once()` confirms the call count but not what was passed. Use `assert_called_once_with(...)` or inspect `mock.call_args` to verify the actual arguments.
6. **Ignoring warnings as errors** -- Configure `filterwarnings = ["error"]` in `pyproject.toml` to catch deprecation warnings early. A passing test suite that emits 50 deprecation warnings is a time bomb.
---
## Related Skills
- `testing/vitest` -- JavaScript/TypeScript testing counterpart
- `languages/python` -- Python language patterns and idioms
- `methodology/test-driven-development` -- TDD workflow for writing tests first
- `devops/github-actions` — Running pytest in CI/CD pipelines
@@ -0,0 +1,248 @@
# pytest Fixture Patterns
Catalog of reusable fixture patterns for common testing scenarios.
## 1. Factory Fixture
Create multiple instances with customizable defaults.
```python
import pytest
from dataclasses import dataclass
@pytest.fixture
def make_user():
"""Factory fixture: creates User instances with sensible defaults."""
created = []
def _make_user(
name: str = "Test User",
email: str | None = None,
is_active: bool = True,
):
if email is None:
email = f"user-{len(created)}@test.com"
user = User(name=name, email=email, is_active=is_active)
created.append(user)
return user
yield _make_user
# Cleanup: delete all created users
for user in created:
user.delete()
```
Usage:
```python
def test_deactivate_user(make_user):
user = make_user(name="Alice", is_active=True)
user.deactivate()
assert not user.is_active
```
## 2. Database Session (SQLAlchemy)
Transaction-isolated database session that rolls back after each test.
```python
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture(scope="session")
def engine():
"""Create a test database engine (once per test session)."""
engine = create_engine("postgresql://test:test@localhost:5432/test_db")
yield engine
engine.dispose()
@pytest.fixture(scope="session")
def tables(engine):
"""Create all tables once, drop after all tests."""
Base.metadata.create_all(engine)
yield
Base.metadata.drop_all(engine)
@pytest.fixture
def db_session(engine, tables):
"""Provide a transactional database session that rolls back after each test."""
connection = engine.connect()
transaction = connection.begin()
session = sessionmaker(bind=connection)()
yield session
session.close()
transaction.rollback()
connection.close()
```
## 3. Temporary Files and Directories
```python
@pytest.fixture
def sample_config(tmp_path: Path) -> Path:
"""Create a temporary config file with test content."""
config = tmp_path / "config.yaml"
config.write_text(
"""\
database:
host: localhost
port: 5432
debug: true
"""
)
return config
@pytest.fixture
def data_dir(tmp_path: Path) -> Path:
"""Create a temporary directory structure for testing."""
(tmp_path / "input").mkdir()
(tmp_path / "output").mkdir()
(tmp_path / "input" / "data.csv").write_text("id,name\n1,Alice\n2,Bob\n")
return tmp_path
```
## 4. Mock External Service
```python
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
@pytest.fixture
def mock_http_client():
"""Mock an HTTP client with pre-configured responses."""
client = MagicMock()
client.get.return_value = MagicMock(
status_code=200,
json=lambda: {"status": "ok"},
)
client.post.return_value = MagicMock(
status_code=201,
json=lambda: {"id": "new-123"},
)
return client
@pytest.fixture
def mock_payment_gateway():
"""Mock a payment gateway service."""
with patch("app.services.payment.PaymentGateway") as mock_cls:
instance = mock_cls.return_value
instance.charge.return_value = {
"transaction_id": "txn-test-123",
"status": "succeeded",
}
instance.refund.return_value = {
"refund_id": "ref-test-456",
"status": "refunded",
}
yield instance
# Async version
@pytest.fixture
def mock_email_service():
"""Mock an async email service."""
with patch("app.services.email.EmailService") as mock_cls:
instance = mock_cls.return_value
instance.send = AsyncMock(return_value={"message_id": "msg-test-789"})
yield instance
```
## 5. Authenticated Test Client (FastAPI)
```python
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
from app.auth import create_access_token
@pytest.fixture
def auth_token():
"""Generate a valid JWT token for testing."""
return create_access_token(
data={"sub": "test-user-id", "role": "admin"},
expires_minutes=60,
)
@pytest.fixture
def auth_headers(auth_token: str) -> dict[str, str]:
"""HTTP headers with Bearer token."""
return {"Authorization": f"Bearer {auth_token}"}
@pytest.fixture
async def client():
"""Unauthenticated async test client."""
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as c:
yield c
@pytest.fixture
async def auth_client(auth_headers):
"""Authenticated async test client."""
transport = ASGITransport(app=app)
async with AsyncClient(
transport=transport,
base_url="http://test",
headers=auth_headers,
) as c:
yield c
```
## 6. Environment Variables
```python
@pytest.fixture
def env_vars(monkeypatch):
"""Set environment variables for the test, automatically restored after."""
monkeypatch.setenv("DATABASE_URL", "postgresql://test:test@localhost/test")
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.delenv("PRODUCTION_API_KEY", raising=False)
```
## 7. Freezing Time
```python
@pytest.fixture
def frozen_time():
"""Freeze time to a specific moment."""
fixed = datetime(2025, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
with patch("app.services.datetime") as mock_dt:
mock_dt.now.return_value = fixed
mock_dt.utcnow.return_value = fixed
mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw)
yield fixed
```
Alternative: use `freezegun` library with `@freeze_time("2025-01-15 12:00:00")`.
## 8. Parametrized Fixture
```python
@pytest.fixture(params=["sqlite", "postgresql"])
def db_url(request):
"""Run tests against multiple database backends."""
urls = {
"sqlite": "sqlite:///test.db",
"postgresql": "postgresql://test:test@localhost/test",
}
return urls[request.param]
```
## Fixture Scope Reference
| Scope | Lifetime | Use For |
|-------|----------|---------|
| `function` (default) | Each test | Most fixtures, mutable state |
| `class` | Each test class | Shared setup for a class |
| `module` | Each test file | Expensive setup shared across file |
| `session` | Entire test run | Database engine, heavy resources |
## Tips
- Use `yield` (not `return`) when cleanup is needed after the test.
- Use `autouse=True` sparingly -- only for things every test needs.
- Keep fixtures small and composable -- combine them in tests, not in other fixtures.
- Use `monkeypatch` instead of `unittest.mock.patch` for env vars and attributes when possible.
- Name fixtures after what they provide, not what they do: `db_session` not `setup_database`.
@@ -0,0 +1,197 @@
"""
Starter conftest.py -- common fixtures for pytest.
Usage:
Place this file at the root of your tests/ directory.
pytest automatically discovers conftest.py and makes its fixtures
available to all tests in the same directory and below.
"""
import os
from collections.abc import AsyncGenerator, Generator
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
# ---------------------------------------------------------------------------
# If using FastAPI + httpx:
# from httpx import ASGITransport, AsyncClient
# from app.main import app
# If using SQLAlchemy:
# from sqlalchemy import create_engine
# from sqlalchemy.orm import Session, sessionmaker
# from app.models import Base
# ---------------------------------------------------------------------------
# ==========================================================================
# Environment Variables
# ==========================================================================
@pytest.fixture(autouse=True)
def _test_env(monkeypatch: pytest.MonkeyPatch) -> None:
"""Set safe default environment variables for all tests.
autouse=True ensures this runs for every test automatically.
monkeypatch restores original values after each test.
"""
monkeypatch.setenv("APP_ENV", "test")
monkeypatch.setenv("DEBUG", "true")
monkeypatch.setenv("SECRET_KEY", "test-secret-not-for-production")
monkeypatch.setenv("DATABASE_URL", "sqlite:///test.db")
# Remove any production secrets that should never leak into tests.
monkeypatch.delenv("PRODUCTION_API_KEY", raising=False)
monkeypatch.delenv("AWS_SECRET_ACCESS_KEY", raising=False)
# ==========================================================================
# Temporary Directory
# ==========================================================================
@pytest.fixture
def data_dir(tmp_path: Path) -> Path:
"""Provide a temporary directory with input/output subdirectories."""
(tmp_path / "input").mkdir()
(tmp_path / "output").mkdir()
return tmp_path
@pytest.fixture
def sample_file(tmp_path: Path) -> Path:
"""Create a sample text file for testing file operations."""
f = tmp_path / "sample.txt"
f.write_text("line 1\nline 2\nline 3\n")
return f
# ==========================================================================
# Database Session (SQLAlchemy)
# ==========================================================================
# Uncomment this section if using SQLAlchemy.
# TEST_DATABASE_URL = os.getenv(
# "TEST_DATABASE_URL", "postgresql://test:test@localhost:5432/test_db"
# )
#
#
# @pytest.fixture(scope="session")
# def engine():
# """Create database engine for the entire test session."""
# eng = create_engine(TEST_DATABASE_URL)
# yield eng
# eng.dispose()
#
#
# @pytest.fixture(scope="session")
# def tables(engine):
# """Create tables at start of session, drop at end."""
# Base.metadata.create_all(engine)
# yield
# Base.metadata.drop_all(engine)
#
#
# @pytest.fixture
# def db_session(engine, tables) -> Generator[Session, None, None]:
# """Transactional database session -- rolls back after each test."""
# connection = engine.connect()
# transaction = connection.begin()
# session = sessionmaker(bind=connection)()
#
# yield session
#
# session.close()
# transaction.rollback()
# connection.close()
# ==========================================================================
# HTTP Test Client (FastAPI)
# ==========================================================================
# Uncomment this section if using FastAPI.
# @pytest.fixture
# async def client() -> AsyncGenerator[AsyncClient, None]:
# """Async HTTP client for testing API endpoints."""
# transport = ASGITransport(app=app)
# async with AsyncClient(transport=transport, base_url="http://test") as c:
# yield c
#
#
# @pytest.fixture
# def auth_headers() -> dict[str, str]:
# """Authorization headers with a test JWT token."""
# from app.auth import create_access_token
# token = create_access_token(data={"sub": "test-user", "role": "admin"})
# return {"Authorization": f"Bearer {token}"}
#
#
# @pytest.fixture
# async def auth_client(auth_headers) -> AsyncGenerator[AsyncClient, None]:
# """Authenticated async HTTP client."""
# transport = ASGITransport(app=app)
# async with AsyncClient(
# transport=transport, base_url="http://test", headers=auth_headers
# ) as c:
# yield c
# ==========================================================================
# Mock External Services
# ==========================================================================
@pytest.fixture
def mock_http_client() -> MagicMock:
"""Generic mock HTTP client with default 200/201 responses."""
client = MagicMock()
client.get.return_value = MagicMock(
status_code=200,
json=lambda: {"status": "ok"},
)
client.post.return_value = MagicMock(
status_code=201,
json=lambda: {"id": "new-123"},
)
return client
# @pytest.fixture
# def mock_email_service():
# """Mock email service to prevent real emails in tests."""
# with patch("app.services.email.send_email") as mock_send:
# mock_send.return_value = {"message_id": "test-msg-001"}
# yield mock_send
# ==========================================================================
# Factory Fixtures
# ==========================================================================
# @pytest.fixture
# def make_user(db_session):
# """Factory fixture: creates User instances with defaults."""
# created = []
#
# def _make_user(
# name: str = "Test User",
# email: str | None = None,
# is_active: bool = True,
# ):
# from app.models import User
# if email is None:
# email = f"user-{len(created)}@test.com"
# user = User(name=name, email=email, is_active=is_active)
# db_session.add(user)
# db_session.flush()
# created.append(user)
# return user
#
# return _make_user
+799 -43
View File
@@ -1,89 +1,845 @@
---
name: vitest
description: >
Trigger this skill whenever writing, debugging, or refactoring JavaScript or TypeScript tests, or when Vitest mocking, coverage, or configuration are mentioned. Activate for any .test.ts, .test.tsx, .test.js, .spec.ts, .spec.js file, vitest.config.ts reference, or React component testing with Testing Library. Also use when the user asks about JS/TS test patterns, test organization, or vi.mock/vi.fn usage.
---
# Vitest
## Description
Modern JavaScript/TypeScript testing with Vitest including mocking and coverage.
## When to Use
- Testing JavaScript/TypeScript
- React component testing
- Unit and integration tests
## When NOT to Use
- Python testing -- use the `testing/pytest` skill instead
- Projects that explicitly mandate Jest-only by convention with no Vitest dependency
- Non-JavaScript/TypeScript projects
---
## Core Patterns
### Basic Tests
### 1. Test Structure
#### describe / it / expect
```typescript
import { describe, it, expect } from 'vitest';
import { formatCurrency } from './format';
describe('math', () => {
it('should add numbers', () => {
expect(1 + 1).toBe(2);
describe('formatCurrency', () => {
it('should format whole dollars', () => {
expect(formatCurrency(100)).toBe('$100.00');
});
it('should throw on invalid input', () => {
expect(() => divide(1, 0)).toThrow('Division by zero');
it('should format cents correctly', () => {
expect(formatCurrency(9.5)).toBe('$9.50');
});
it('should handle zero', () => {
expect(formatCurrency(0)).toBe('$0.00');
});
it('should throw on negative values', () => {
expect(() => formatCurrency(-5)).toThrow('Amount must be non-negative');
});
});
```
### Mocking
#### Lifecycle Hooks
```typescript
import { vi, describe, it, expect } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
import { Database } from './database';
// Mock module
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1 })
describe('UserRepository', () => {
let db: Database;
beforeAll(async () => {
// Runs once before all tests in this describe block
db = await Database.connect('test://localhost/testdb');
await db.migrate();
});
afterAll(async () => {
await db.disconnect();
});
beforeEach(async () => {
// Runs before each test
await db.seed({ users: [{ id: 1, name: 'Alice' }] });
});
afterEach(async () => {
await db.truncate('users');
});
it('should find user by id', async () => {
const user = await db.users.findById(1);
expect(user).toEqual({ id: 1, name: 'Alice' });
});
it('should return null for missing user', async () => {
const user = await db.users.findById(999);
expect(user).toBeNull();
});
});
```
#### test.each for Parametrized Tests
```typescript
import { describe, it, expect, test } from 'vitest';
import { validateEmail } from './validators';
describe('validateEmail', () => {
test.each([
{ email: 'user@example.com', expected: true },
{ email: 'admin@test.org', expected: true },
{ email: 'name+tag@domain.co.uk', expected: true },
])('should accept valid email: $email', ({ email, expected }) => {
expect(validateEmail(email)).toBe(expected);
});
test.each([
{ email: '', reason: 'empty string' },
{ email: 'no-at-sign', reason: 'missing @' },
{ email: '@no-local.com', reason: 'missing local part' },
{ email: 'spaces in@email.com', reason: 'contains spaces' },
])('should reject invalid email ($reason): $email', ({ email }) => {
expect(validateEmail(email)).toBe(false);
});
});
```
#### Nested describe Blocks
```typescript
describe('ShoppingCart', () => {
describe('when empty', () => {
it('should have zero total', () => {
const cart = new ShoppingCart();
expect(cart.total()).toBe(0);
});
it('should have zero item count', () => {
const cart = new ShoppingCart();
expect(cart.itemCount()).toBe(0);
});
});
describe('with items', () => {
let cart: ShoppingCart;
beforeEach(() => {
cart = new ShoppingCart();
cart.add({ name: 'Widget', price: 9.99, quantity: 2 });
cart.add({ name: 'Gadget', price: 24.99, quantity: 1 });
});
it('should calculate total', () => {
expect(cart.total()).toBeCloseTo(44.97);
});
it('should count all items', () => {
expect(cart.itemCount()).toBe(3);
});
});
});
```
---
### 2. Mocking
#### vi.mock for Module Mocking
```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { sendWelcomeEmail } from './onboarding';
// Mock the entire email module -- hoisted to the top of the file automatically
vi.mock('./email', () => ({
sendEmail: vi.fn().mockResolvedValue({ messageId: 'msg-123' }),
}));
// Mock function
const callback = vi.fn();
callback('arg');
expect(callback).toHaveBeenCalledWith('arg');
```
// Import AFTER vi.mock declaration
import { sendEmail } from './email';
### Async Tests
describe('sendWelcomeEmail', () => {
beforeEach(() => {
vi.clearAllMocks();
});
```typescript
it('should fetch data', async () => {
const data = await fetchData();
expect(data).toEqual({ id: 1 });
});
it('should send email with welcome template', async () => {
await sendWelcomeEmail('alice@example.com');
it('should reject on error', async () => {
await expect(fetchData()).rejects.toThrow('Error');
expect(sendEmail).toHaveBeenCalledWith({
to: 'alice@example.com',
template: 'welcome',
subject: 'Welcome to our platform!',
});
});
it('should return the message id', async () => {
const result = await sendWelcomeEmail('alice@example.com');
expect(result.messageId).toBe('msg-123');
});
});
```
### React Testing
#### vi.fn for Function Spies
```typescript
import { describe, it, expect, vi } from 'vitest';
describe('EventEmitter', () => {
it('should call listener on emit', () => {
const emitter = new EventEmitter();
const listener = vi.fn();
emitter.on('click', listener);
emitter.emit('click', { x: 10, y: 20 });
expect(listener).toHaveBeenCalledOnce();
expect(listener).toHaveBeenCalledWith({ x: 10, y: 20 });
});
it('should track multiple calls', () => {
const callback = vi.fn();
callback('first');
callback('second');
callback('third');
expect(callback).toHaveBeenCalledTimes(3);
expect(callback.mock.calls).toEqual([['first'], ['second'], ['third']]);
});
});
```
#### vi.spyOn
```typescript
import { describe, it, expect, vi, afterEach } from 'vitest';
import * as mathUtils from './math-utils';
describe('calculateTax', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('should use the tax rate function', () => {
const spy = vi.spyOn(mathUtils, 'getTaxRate').mockReturnValue(0.08);
const result = calculateTax(100);
expect(spy).toHaveBeenCalledWith();
expect(result).toBe(8);
});
it('should spy without changing behavior', () => {
const spy = vi.spyOn(console, 'warn');
triggerDeprecationWarning();
expect(spy).toHaveBeenCalledWith(
expect.stringContaining('deprecated')
);
});
});
```
#### mockResolvedValue / mockRejectedValue
```typescript
import { describe, it, expect, vi } from 'vitest';
describe('UserService', () => {
it('should return user on successful fetch', async () => {
const fetchUser = vi.fn().mockResolvedValue({ id: 1, name: 'Alice' });
const user = await fetchUser(1);
expect(user).toEqual({ id: 1, name: 'Alice' });
});
it('should throw on failed fetch', async () => {
const fetchUser = vi.fn().mockRejectedValue(new Error('User not found'));
await expect(fetchUser(999)).rejects.toThrow('User not found');
});
it('should return different values on successive calls', async () => {
const getToken = vi.fn()
.mockResolvedValueOnce('token-1')
.mockResolvedValueOnce('token-2')
.mockRejectedValueOnce(new Error('Expired'));
expect(await getToken()).toBe('token-1');
expect(await getToken()).toBe('token-2');
await expect(getToken()).rejects.toThrow('Expired');
});
});
```
#### MSW (Mock Service Worker) for API Mocking
```typescript
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import { fetchUsers } from './api-client';
const server = setupServer(
http.get('https://api.example.com/users', () => {
return HttpResponse.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
]);
}),
http.post('https://api.example.com/users', async ({ request }) => {
const body = await request.json() as { name: string };
return HttpResponse.json(
{ id: 3, name: body.name },
{ status: 201 }
);
})
);
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('API Client', () => {
it('should fetch users', async () => {
const users = await fetchUsers();
expect(users).toHaveLength(2);
expect(users[0].name).toBe('Alice');
});
it('should handle server errors', async () => {
server.use(
http.get('https://api.example.com/users', () => {
return HttpResponse.json(
{ message: 'Internal Server Error' },
{ status: 500 }
);
})
);
await expect(fetchUsers()).rejects.toThrow('Server error');
});
});
```
---
### 3. React Testing
#### Render and Query
```tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { Greeting } from './Greeting';
it('should handle click', async () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Click</Button>);
describe('Greeting', () => {
it('should display the user name', () => {
render(<Greeting name="Alice" />);
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalled();
// getBy* throws if not found -- use for elements that must exist
expect(screen.getByText('Hello, Alice!')).toBeInTheDocument();
});
it('should not display admin badge for regular users', () => {
render(<Greeting name="Alice" role="viewer" />);
// queryBy* returns null if not found -- use for asserting absence
expect(screen.queryByText('Admin')).not.toBeInTheDocument();
});
it('should display admin badge for admins', () => {
render(<Greeting name="Alice" role="admin" />);
expect(screen.getByText('Admin')).toBeInTheDocument();
});
});
```
#### userEvent for Interactions
```tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('should submit credentials', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText('Email'), 'alice@example.com');
await user.type(screen.getByLabelText('Password'), 'secret123');
await user.click(screen.getByRole('button', { name: 'Sign In' }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'alice@example.com',
password: 'secret123',
});
});
it('should show validation error on empty submit', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={vi.fn()} />);
await user.click(screen.getByRole('button', { name: 'Sign In' }));
expect(screen.getByText('Email is required')).toBeInTheDocument();
});
it('should toggle password visibility', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={vi.fn()} />);
const passwordInput = screen.getByLabelText('Password');
expect(passwordInput).toHaveAttribute('type', 'password');
await user.click(screen.getByRole('button', { name: 'Show password' }));
expect(passwordInput).toHaveAttribute('type', 'text');
});
});
```
#### findBy for Async Rendering and waitFor
```tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserProfile } from './UserProfile';
describe('UserProfile', () => {
it('should load and display user data', async () => {
render(<UserProfile userId={1} />);
// findBy* waits for the element to appear (async query)
const heading = await screen.findByRole('heading', { name: 'Alice' });
expect(heading).toBeInTheDocument();
});
it('should show loading state initially', () => {
render(<UserProfile userId={1} />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('should update after action', async () => {
const user = userEvent.setup();
render(<UserProfile userId={1} />);
await screen.findByRole('heading', { name: 'Alice' });
await user.click(screen.getByRole('button', { name: 'Deactivate' }));
await waitFor(() => {
expect(screen.getByText('Status: Inactive')).toBeInTheDocument();
});
});
});
```
#### Testing with Context Providers
```tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { ThemeProvider } from './ThemeContext';
import { ThemedButton } from './ThemedButton';
function renderWithProviders(ui: React.ReactElement, options?: { theme?: 'light' | 'dark' }) {
const theme = options?.theme ?? 'light';
return render(
<ThemeProvider value={theme}>
{ui}
</ThemeProvider>
);
}
describe('ThemedButton', () => {
it('should apply light theme styles', () => {
renderWithProviders(<ThemedButton>Click me</ThemedButton>, { theme: 'light' });
expect(screen.getByRole('button')).toHaveClass('btn-light');
});
it('should apply dark theme styles', () => {
renderWithProviders(<ThemedButton>Click me</ThemedButton>, { theme: 'dark' });
expect(screen.getByRole('button')).toHaveClass('btn-dark');
});
});
```
---
### 4. Async Testing
#### Promises and async/await
```typescript
import { describe, it, expect } from 'vitest';
import { fetchUser, processQueue } from './services';
describe('async operations', () => {
it('should resolve with user data', async () => {
const user = await fetchUser(1);
expect(user).toEqual({ id: 1, name: 'Alice' });
});
it('should reject with descriptive error', async () => {
await expect(fetchUser(-1)).rejects.toThrow('Invalid user ID');
});
it('should process all items in queue', async () => {
const results = await processQueue(['a', 'b', 'c']);
expect(results).toHaveLength(3);
expect(results.every((r) => r.status === 'done')).toBe(true);
});
});
```
#### Fake Timers
```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { debounce } from './debounce';
describe('debounce', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should not call function before delay', () => {
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced();
vi.advanceTimersByTime(200);
expect(fn).not.toHaveBeenCalled();
});
it('should call function after delay', () => {
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced();
vi.advanceTimersByTime(300);
expect(fn).toHaveBeenCalledOnce();
});
it('should reset timer on subsequent calls', () => {
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced();
vi.advanceTimersByTime(200);
debounced(); // reset
vi.advanceTimersByTime(200);
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(100);
expect(fn).toHaveBeenCalledOnce();
});
});
```
#### Fake Timers with Date
```typescript
import { describe, it, expect, vi } from 'vitest';
import { isExpired } from './token';
describe('isExpired', () => {
it('should detect expired tokens', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2025-06-15T12:00:00Z'));
const token = { expiresAt: '2025-06-15T11:00:00Z' };
expect(isExpired(token)).toBe(true);
vi.useRealTimers();
});
});
```
---
### 5. Snapshot Testing
#### toMatchSnapshot
```typescript
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import { Badge } from './Badge';
describe('Badge', () => {
it('should match snapshot for success variant', () => {
const { container } = render(<Badge variant="success">Active</Badge>);
expect(container.firstChild).toMatchSnapshot();
});
});
```
#### toMatchInlineSnapshot
Inline snapshots embed the expected value directly in the test file. Vitest updates them automatically on first run.
```typescript
import { describe, it, expect } from 'vitest';
import { formatError } from './errors';
describe('formatError', () => {
it('should format validation error', () => {
const error = formatError({ field: 'email', rule: 'required' });
expect(error).toMatchInlineSnapshot(`
{
"code": "VALIDATION_ERROR",
"field": "email",
"message": "email is required",
}
`);
});
});
```
#### When to Use Snapshots (and When Not To)
**Use snapshots for:**
- Serialized output that is tedious to write by hand (large objects, rendered markup)
- Catching unintended changes in generated output
- Error message formatting
**Do not use snapshots for:**
- Business logic assertions -- write explicit `expect(value).toBe(expected)` instead
- Frequently changing output -- snapshot churn leads to mindless updates
- Large component trees -- a small change deep in the tree makes the diff unreadable; test specific elements instead
---
### 6. Coverage
#### vitest.config.ts Coverage Settings
```typescript
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8', // or 'istanbul'
reporter: ['text', 'html', 'lcov'],
reportsDirectory: './coverage',
include: ['src/**/*.{ts,tsx}'],
exclude: [
'src/**/*.test.{ts,tsx}',
'src/**/*.d.ts',
'src/**/index.ts', // barrel files
'src/test-utils/**',
],
thresholds: {
statements: 80,
branches: 80,
functions: 80,
lines: 80,
},
},
},
});
```
#### Running Coverage
```bash
vitest run --coverage # Run once with coverage
vitest --coverage # Watch mode with coverage
vitest run --coverage.provider=v8 # Override provider via CLI
```
#### Per-File Thresholds
```typescript
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
thresholds: {
// Global thresholds
statements: 80,
// Per-glob overrides for critical paths
'src/auth/**': {
statements: 95,
branches: 95,
},
},
},
},
});
```
---
### 7. Setup and Configuration
#### vitest.config.ts Basics
```typescript
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true, // Use describe/it/expect without imports
environment: 'jsdom', // DOM environment for React (or 'happy-dom')
setupFiles: ['./src/test-setup.ts'],
include: ['src/**/*.test.{ts,tsx}'],
exclude: ['node_modules', 'dist', 'e2e'],
testTimeout: 10_000,
hookTimeout: 30_000,
},
resolve: {
alias: {
'@': '/src',
},
},
});
```
#### Setup File
```typescript
// src/test-setup.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
// Automatic cleanup after each test
afterEach(() => {
cleanup();
});
```
#### Workspace Configuration
For monorepos with multiple packages:
```typescript
// vitest.workspace.ts
import { defineWorkspace } from 'vitest/config';
export default defineWorkspace([
{
extends: './vitest.config.ts',
test: {
name: 'ui',
include: ['packages/ui/**/*.test.{ts,tsx}'],
environment: 'jsdom',
},
},
{
extends: './vitest.config.ts',
test: {
name: 'api',
include: ['packages/api/**/*.test.ts'],
environment: 'node',
},
},
]);
```
#### Environment Per File
Use a magic comment at the top of a test file to override the environment:
```typescript
// @vitest-environment happy-dom
import { describe, it, expect } from 'vitest';
describe('DOM-heavy tests', () => {
it('should create elements', () => {
const div = document.createElement('div');
div.textContent = 'Hello';
expect(div.textContent).toBe('Hello');
});
});
```
#### Globals Mode
When `globals: true` is set in config, you do not need to import `describe`, `it`, `expect`, `vi`, etc. Add the types to `tsconfig.json`:
```json
{
"compilerOptions": {
"types": ["vitest/globals"]
}
}
```
---
## Best Practices
1. Use describe blocks for grouping
2. Prefer async/await for async tests
3. Use userEvent over fireEvent
4. Mock at module boundaries
5. Clean up after tests
1. **Use `userEvent` over `fireEvent`** -- `userEvent` simulates real user behavior (focus, keystrokes, blur) while `fireEvent` dispatches raw DOM events. `userEvent` catches bugs that `fireEvent` misses, such as disabled buttons still receiving clicks.
2. **Query by role and label, not test IDs** -- Prefer `getByRole('button', { name: 'Submit' })` and `getByLabelText('Email')` over `getByTestId('submit-btn')`. Accessible queries validate your markup and are resilient to refactors.
3. **Clear mocks between tests** -- Call `vi.clearAllMocks()` in `beforeEach` or `vi.restoreAllMocks()` in `afterEach`. Leaked mock state between tests causes order-dependent failures that are painful to debug.
4. **Keep tests focused on one behavior** -- Each `it` block should test a single user-observable behavior. If your test description contains "and", split it into two tests.
5. **Avoid testing implementation details** -- Do not assert on component state, internal method calls, or private variables. Test what the user sees and what the component outputs. Implementation tests break on every refactor without catching real bugs.
6. **Use MSW for network mocking over vi.mock on fetch** -- MSW intercepts at the network level, so your tests exercise the actual fetch/axios code paths. Mocking `fetch` directly skips serialization, headers, and error handling logic.
7. **Colocate tests with source files** -- Place `Button.test.tsx` next to `Button.tsx`. This makes it obvious which files have tests and simplifies imports. Reserve a top-level `e2e/` folder only for end-to-end tests.
8. **Run tests in watch mode during development** -- `vitest` (no flags) starts in watch mode and re-runs only affected tests on file change. Use `vitest run` in CI for a single full run with exit code.
---
## Common Pitfalls
- **Not awaiting async**: Always await promises
- **Stale mocks**: Clear mocks between tests
- **Testing implementation**: Test behavior
1. **Forgetting to await userEvent calls** -- Every `userEvent` method is async. Omitting `await` causes the assertion to run before the interaction completes, leading to false passes or intermittent failures.
2. **vi.mock hoisting confusion** -- `vi.mock()` calls are hoisted to the top of the file. If you define a mock implementation that references a variable declared below the `vi.mock` call, it will be `undefined`. Use `vi.mock` with a factory function or move the variable above.
3. **Not cleaning up after fake timers** -- Forgetting `vi.useRealTimers()` in `afterEach` causes subsequent tests to silently use fake timers, producing mysterious timeouts and passing tests that should fail.
4. **Using `getBy` queries for elements that may not exist** -- `getByText('Error')` throws immediately if the element is absent. When asserting that something is NOT rendered, use `queryByText('Error')` which returns `null`.
5. **Snapshot overuse** -- Developers update snapshots without reviewing the diff. Over time, snapshots become rubber stamps. Limit snapshots to serialized output and error formatting; use explicit assertions for behavior.
6. **Testing third-party library internals** -- Do not test that React Router navigates correctly or that Zustand updates state. Test that your component renders the right thing after navigation or state change. Trust library authors; test your code.
---
## Related Skills
- `testing/pytest` -- Python testing counterpart
- `languages/typescript` -- TypeScript language patterns and strict typing
- `frameworks/react` -- React component patterns for component testing
- `methodology/test-driven-development` -- TDD workflow for writing tests first
- `devops/github-actions` — Running vitest in CI/CD pipelines
@@ -0,0 +1,242 @@
# Vitest Mock Patterns
Catalog of mocking patterns for common testing scenarios.
## 1. Module Mock (Full)
Replace an entire module with mock implementations.
```typescript
import { describe, it, expect, vi } from "vitest";
// Mock the entire module BEFORE importing code that uses it.
vi.mock("@/services/payment", () => ({
chargeCard: vi.fn().mockResolvedValue({
transactionId: "txn-123",
status: "succeeded",
}),
refundCharge: vi.fn().mockResolvedValue({
refundId: "ref-456",
status: "refunded",
}),
}));
import { chargeCard } from "@/services/payment";
import { checkout } from "@/services/checkout";
describe("checkout", () => {
it("should charge the card and return success", async () => {
const result = await checkout({ amount: 42, cardToken: "tok_test" });
expect(chargeCard).toHaveBeenCalledWith({
amount: 42,
token: "tok_test",
});
expect(result.status).toBe("succeeded");
});
});
```
## 2. Partial Module Mock
Mock only specific exports; keep the rest real.
```typescript
import { describe, it, expect, vi } from "vitest";
vi.mock("@/utils/config", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/utils/config")>();
return {
...actual,
// Override only this one export
getFeatureFlag: vi.fn().mockReturnValue(true),
};
});
import { getFeatureFlag, parseConfig } from "@/utils/config";
describe("with feature flag enabled", () => {
it("should use the new algorithm", () => {
// getFeatureFlag is mocked, parseConfig is real
expect(getFeatureFlag("new-algo")).toBe(true);
});
});
```
## 3. Manual Mock Reset / Per-Test Overrides
```typescript
import { describe, it, expect, vi, beforeEach } from "vitest";
import { fetchUser } from "@/api/users";
vi.mock("@/api/users");
// Type the mock for autocomplete
const mockFetchUser = vi.mocked(fetchUser);
beforeEach(() => {
vi.resetAllMocks(); // Clear call history AND implementations
});
describe("user profile", () => {
it("shows user data on success", async () => {
mockFetchUser.mockResolvedValueOnce({ id: "1", name: "Alice" });
// ...test
});
it("shows error on failure", async () => {
mockFetchUser.mockRejectedValueOnce(new Error("Network error"));
// ...test
});
});
```
## 4. API Mock with MSW (Mock Service Worker)
Best for integration tests that should exercise real fetch/axios code.
```typescript
// test/mocks/handlers.ts
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("/api/users/:id", ({ params }) => {
return HttpResponse.json({ id: params.id, name: "Alice", email: "alice@example.com" });
}),
http.post("/api/users", async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: "new-1", ...body }, { status: 201 });
}),
];
```
```typescript
// test/mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);
```
```typescript
// test/setup.ts (referenced in vitest.config.ts setupFiles)
import { afterAll, afterEach, beforeAll } from "vitest";
import { server } from "./mocks/server";
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
```
```typescript
// Usage in tests -- override handlers per test
import { http, HttpResponse } from "msw";
import { server } from "../mocks/server";
it("handles server error", async () => {
server.use(
http.get("/api/users/:id", () => {
return HttpResponse.json({ error: "Not found" }, { status: 404 });
}),
);
// ...test error handling
});
```
## 5. Timer Mocks
Control `setTimeout`, `setInterval`, `Date.now`.
```typescript
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it("should call the function after the delay", () => {
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced();
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(300);
expect(fn).toHaveBeenCalledOnce();
});
// Fake date: vi.setSystemTime(new Date("2025-01-15T12:00:00Z"))
```
## 6. Spy Patterns
Observe calls without replacing implementation.
```typescript
import { describe, it, expect, vi } from "vitest";
describe("logging", () => {
it("should log errors to console", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
logError("something went wrong");
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("something went wrong"),
);
consoleSpy.mockRestore();
});
});
// Spy on object method without changing behavior
it("should call save", () => {
const repo = new UserRepository();
const saveSpy = vi.spyOn(repo, "save");
repo.createUser({ name: "Alice" });
expect(saveSpy).toHaveBeenCalledOnce();
expect(saveSpy).toHaveBeenCalledWith(expect.objectContaining({ name: "Alice" }));
});
```
## 7. Global / Window Mocks
```typescript
// Mock window.location
vi.spyOn(window, "location", "get").mockReturnValue({ ...window.location, pathname: "/dashboard" });
// Mock localStorage
const storage: Record<string, string> = {};
vi.spyOn(Storage.prototype, "getItem").mockImplementation((key) => storage[key] ?? null);
vi.spyOn(Storage.prototype, "setItem").mockImplementation((key, val) => { storage[key] = val; });
// Mock fetch (when not using MSW)
global.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ data: "test" }) });
```
## 8. Class Mock
```typescript
vi.mock("@/services/analytics", () => ({
AnalyticsClient: vi.fn().mockImplementation(() => ({
track: vi.fn(),
identify: vi.fn(),
flush: vi.fn().mockResolvedValue(undefined),
})),
}));
```
## Quick Reference: Mock Functions
| Method | Purpose |
|--------|---------|
| `vi.fn()` | Create a standalone mock function |
| `vi.fn().mockReturnValue(x)` | Always return `x` |
| `vi.fn().mockReturnValueOnce(x)` | Return `x` once, then default |
| `vi.fn().mockResolvedValue(x)` | Return `Promise.resolve(x)` |
| `vi.fn().mockRejectedValue(e)` | Return `Promise.reject(e)` |
| `vi.fn().mockImplementation(fn)` | Use custom implementation |
| `vi.spyOn(obj, "method")` | Spy on existing method |
| `vi.mocked(fn)` | Type helper for mocked function |
| `vi.mock("module")` | Auto-mock all exports |
| `vi.resetAllMocks()` | Reset history and implementations |
| `vi.restoreAllMocks()` | Restore original implementations |
| `vi.clearAllMocks()` | Clear call history only |
@@ -0,0 +1,100 @@
/// <reference types="vitest/config" />
import { defineConfig } from "vitest/config";
import path from "node:path";
export default defineConfig({
// -------------------------------------------------------------------------
// Path aliases -- must match tsconfig.json "paths"
// -------------------------------------------------------------------------
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
"@test": path.resolve(__dirname, "test"),
},
},
test: {
// -----------------------------------------------------------------------
// Environment
// -----------------------------------------------------------------------
// "node" -- default, for backend / library code
// "jsdom" -- for code that accesses DOM APIs (React, etc.)
// "happy-dom" -- faster jsdom alternative
environment: "jsdom",
// -----------------------------------------------------------------------
// Globals
// -----------------------------------------------------------------------
// Set to true to use describe/it/expect without importing from "vitest".
// Requires adding "vitest/globals" to tsconfig "types".
globals: true,
// -----------------------------------------------------------------------
// Setup files -- run before each test file
// -----------------------------------------------------------------------
setupFiles: [
"./test/setup.ts",
// "./test/mocks/server.ts", // MSW server setup
],
// -----------------------------------------------------------------------
// File patterns
// -----------------------------------------------------------------------
include: [
"src/**/*.{test,spec}.{ts,tsx}",
"test/**/*.{test,spec}.{ts,tsx}",
],
exclude: [
"node_modules",
"dist",
"e2e/**",
],
// -----------------------------------------------------------------------
// Coverage
// -----------------------------------------------------------------------
coverage: {
provider: "v8", // or "istanbul"
reporter: ["text", "text-summary", "lcov", "json"],
reportsDirectory: "./coverage",
include: ["src/**/*.{ts,tsx}"],
exclude: [
"src/**/*.d.ts",
"src/**/*.test.{ts,tsx}",
"src/**/*.spec.{ts,tsx}",
"src/**/index.ts", // barrel files
"src/types/**",
],
// Minimum thresholds -- fail if coverage drops below these.
thresholds: {
statements: 80,
branches: 80,
functions: 80,
lines: 80,
},
},
// -----------------------------------------------------------------------
// Timeouts
// -----------------------------------------------------------------------
testTimeout: 10_000, // 10s per test
hookTimeout: 10_000, // 10s per beforeEach/afterEach
// -----------------------------------------------------------------------
// Reporters
// -----------------------------------------------------------------------
reporters: ["default"],
// For CI, add JUnit output:
// reporters: ["default", "junit"],
// outputFile: { junit: "./junit.xml" },
// -----------------------------------------------------------------------
// Other options
// -----------------------------------------------------------------------
// restoreMocks: true, // Automatically restore mocks after each test
// clearMocks: true, // Clear mock call history after each test
// mockReset: true, // Reset mocks (clear + remove implementations)
},
});
+90 -22
View File
@@ -6,7 +6,7 @@ A comprehensive toolkit for Claude Code to accelerate development workflows for
- **20 Specialized Agents** - From planning to deployment
- **27+ Slash Commands** - Workflow automation with flag support
- **30+ Skills** - Framework, language, methodology, and optimization expertise
- **38 Skills** - Framework, language, methodology, patterns, and optimization expertise (all with YAML frontmatter and bundled resources)
- **7 Behavioral Modes** - Task-specific response optimization
- **Command Flag System** - Combinable `--flag` syntax for customization
- **Token Optimization** - 30-70% cost savings with compressed output modes
@@ -29,11 +29,18 @@ A comprehensive toolkit for Claude Code to accelerate development workflows for
├── commands/ # 27+ workflow commands
├── modes/ # 7 behavioral mode definitions
├── mcp/ # MCP server configurations
└── skills/ # Framework, language, and methodology skills
├── frameworks/ # FastAPI, Next.js, React, etc.
└── skills/ # 38 skills with YAML frontmatter & bundled resources
├── api/ # OpenAPI specification patterns
├── databases/ # PostgreSQL, MongoDB
├── devops/ # Docker, GitHub Actions
├── frameworks/ # FastAPI, Django, Next.js, React
├── frontend/ # Tailwind CSS, shadcn/ui
├── languages/ # Python, TypeScript, JavaScript
├── methodology/ # TDD, debugging, planning (14 skills)
── optimization/ # Token efficiency patterns
├── methodology/ # TDD, debugging, planning, review (14 skills)
── optimization/ # Token efficiency patterns
├── patterns/ # Error handling, state, logging, caching, auth, API client
├── security/ # OWASP security patterns
└── testing/ # pytest, vitest
```
## Agents
@@ -112,34 +119,55 @@ A comprehensive toolkit for Claude Code to accelerate development workflows for
/spawn [task] # Launch parallel background task
```
## Skills
## Skills (38 Total)
Every skill includes YAML frontmatter for reliable triggering, "When to Use" / "When NOT to Use" sections, core patterns with code examples, best practices, common pitfalls, cross-references, and bundled resources (reference docs, templates, scripts).
### Languages
- Python, TypeScript, JavaScript
- **Python** — Type hints, async, dataclasses, Pydantic, decorators, pattern matching
- **TypeScript** — Advanced types, generics, Zod, discriminated unions, branded types
- **JavaScript** — ES6+, async patterns, Proxy/Reflect, generators, modules
### Frameworks
- FastAPI, Django, Next.js, React
- **FastAPI** — Routes, dependency injection, middleware, WebSocket, testing
- **Django** — ORM, views, migrations, DRF, signals, admin
- **Next.js** — App Router, server/client components, caching, middleware
- **React** — Hooks, custom hooks, context, Suspense, error boundaries, performance
### Databases
- PostgreSQL, MongoDB
- **PostgreSQL** — Schema, indexing (B-tree/GIN/GiST), migrations, CTEs, JSONB
- **MongoDB** — Schema design, aggregation pipelines, indexing, transactions
### DevOps
- Docker, GitHub Actions
- **Docker** — Multi-stage builds, Compose, security hardening, layer caching
- **GitHub Actions** — CI/CD, matrix strategy, reusable workflows, deployment
### Frontend
- Tailwind CSS, shadcn/ui
- **Tailwind CSS** — Responsive, dark mode, animations, theme customization
- **shadcn/ui** — Components, forms, data tables, theming, toast
### API
- **OpenAPI** — 3.1 spec, pagination, versioning, error schemas, webhooks
### Security
- OWASP best practices
- **OWASP** — Top 10, auth, CORS, CSP, secret management, rate limiting
### Testing
- pytest, vitest
- **pytest** — Fixtures, parametrize, mocking, async, coverage
- **vitest** — React Testing Library, mocking, MSW, snapshots, configuration
### Optimization
- Token-efficient output patterns
- Sequential thinking methodology
- **Token-efficient** — Compressed output modes (30-70% cost savings)
### Methodology (Superpowers)
### Developer Patterns (New)
- **error-handling** — Custom errors, retry patterns, Result type, error boundaries
- **state-management** — React state, Zustand, TanStack Query, form state, URL state
- **logging** — Structured logging, log levels, correlation IDs, redaction
- **caching** — Memoization, HTTP cache, Redis, CDN, cache invalidation
- **api-client** — HTTP clients, interceptors, retry, type-safe clients
- **authentication** — JWT, OAuth2, sessions, RBAC, MFA, password hashing
### Methodology (14 Skills)
| Category | Skills |
|----------|--------|
@@ -147,6 +175,7 @@ A comprehensive toolkit for Claude Code to accelerate development workflows for
| **Testing** | test-driven-development, verification-before-completion, testing-anti-patterns |
| **Debugging** | systematic-debugging, root-cause-tracing, defense-in-depth |
| **Collaboration** | dispatching-parallel-agents, requesting-code-review, receiving-code-review, finishing-development-branch |
| **Reasoning** | sequential-thinking |
Key methodology principles:
- **TDD Strict**: No production code without failing test first
@@ -155,6 +184,16 @@ Key methodology principles:
- **Bite-sized Tasks**: 2-5 minute increments with exact code
- **Sequential Thinking**: Step-by-step reasoning with confidence scores
### Bundled Resources
Skills include progressive-disclosure resources loaded on demand:
| Resource Type | Purpose | Examples |
|---------------|---------|----------|
| **references/** | Cheat sheets, decision trees, pattern catalogs | OWASP Top 10, index decision tree, auth flows |
| **templates/** | Starter files, boilerplate, configs | OpenAPI spec, Dockerfile, CI workflows, conftest.py |
| **scripts/** | Executable helpers for deterministic tasks | Security audit scanner, OpenAPI validator |
## Behavioral Modes
Switch modes to optimize responses for different task types:
@@ -307,16 +346,45 @@ Use $ARGUMENTS for command arguments.
Create a new skill in `.claude/skills/category/skillname/SKILL.md`:
```markdown
# Skill Name
```yaml
---
name: my-skill
description: >
What this skill does and when to trigger it. Be specific — list
contexts, keywords, and scenarios. 2-4 pushy sentences.
---
```
## Description
Brief description for matching.
```markdown
# My Skill
Brief overview.
## When to Use
- Scenario 1
- Scenario 2
## When NOT to Use
- Anti-trigger scenario
---
## Patterns
Your patterns and examples here.
## Core Patterns
### Pattern Name
Code examples with good/bad comparisons.
## Best Practices
## Common Pitfalls
## Related Skills
```
Optionally add bundled resources:
```
my-skill/
├── SKILL.md
├── references/ # Loaded into context on demand
├── scripts/ # Executed without loading into context
└── templates/ # Scaffolded into user's project
```
## Workflow Chains