rex-claude 6.0.0 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/{preload-I3MYBVNU.js → preload-OJJO66IG.js} +49 -2
- package/dist/skills/api-design/SKILL.md +134 -0
- package/dist/skills/auth-patterns/SKILL.md +145 -0
- package/dist/skills/db-design/SKILL.md +139 -0
- package/dist/skills/error-handling/SKILL.md +196 -0
- package/dist/skills/i18n/SKILL.md +159 -0
- package/dist/skills/perf/SKILL.md +108 -0
- package/dist/skills/seo/SKILL.md +158 -0
- package/dist/skills/test-strategy/SKILL.md +135 -0
- package/dist/skills/ui-craft/SKILL.md +151 -0
- package/dist/skills/ui-review/SKILL.md +122 -0
- package/dist/skills/ux-flow/SKILL.md +97 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -638,7 +638,7 @@ ${COLORS.bold}${projects.length} projects found${COLORS.reset}
|
|
|
638
638
|
break;
|
|
639
639
|
}
|
|
640
640
|
case "preload": {
|
|
641
|
-
const { preload } = await import("./preload-
|
|
641
|
+
const { preload } = await import("./preload-OJJO66IG.js");
|
|
642
642
|
const cwd = process.argv[3] || process.cwd();
|
|
643
643
|
const context = await preload(cwd);
|
|
644
644
|
if (context) console.log(context);
|
|
@@ -14,9 +14,50 @@ import "./chunk-PDX44BCA.js";
|
|
|
14
14
|
// src/preload.ts
|
|
15
15
|
import Database from "better-sqlite3";
|
|
16
16
|
import * as sqliteVec from "sqlite-vec";
|
|
17
|
-
import { existsSync } from "fs";
|
|
17
|
+
import { existsSync, readFileSync } from "fs";
|
|
18
|
+
import { join } from "path";
|
|
18
19
|
var log = createLogger("preload");
|
|
19
|
-
var MAX_TOKENS =
|
|
20
|
+
var MAX_TOKENS = 300;
|
|
21
|
+
var SKILL_RULES = [
|
|
22
|
+
// Frontend frameworks → UI/UX skills
|
|
23
|
+
{ deps: ["next", "react", "vue", "nuxt", "@angular/core"], skills: ["ux-flow", "ui-craft", "ui-review"] },
|
|
24
|
+
// API layers → API design
|
|
25
|
+
{ deps: ["express", "fastify", "hono", "koa", "@hapi/hapi"], skills: ["api-design", "error-handling"] },
|
|
26
|
+
// Database ORMs → DB design
|
|
27
|
+
{ deps: ["drizzle-orm", "prisma", "typeorm", "mongoose", "sequelize", "knex"], skills: ["db-design"] },
|
|
28
|
+
// Auth libraries → auth patterns
|
|
29
|
+
{ deps: ["next-auth", "lucia", "passport", "jose", "jsonwebtoken", "@auth/core"], skills: ["auth-patterns"] },
|
|
30
|
+
// i18n
|
|
31
|
+
{ deps: ["next-intl", "i18next", "react-i18next", "vue-i18n"], skills: ["i18n"] },
|
|
32
|
+
// Testing
|
|
33
|
+
{ deps: ["vitest", "jest", "@testing-library/react", "playwright", "cypress"], skills: ["test-strategy"] },
|
|
34
|
+
// Next.js specifically → SEO worth mentioning
|
|
35
|
+
{ deps: ["next"], skills: ["seo", "perf"] },
|
|
36
|
+
// Any project → performance
|
|
37
|
+
{ deps: ["react", "vue", "angular"], skills: ["perf"] }
|
|
38
|
+
];
|
|
39
|
+
function detectRelevantSkills(projectRoot) {
|
|
40
|
+
const pkgPath = join(projectRoot, "package.json");
|
|
41
|
+
if (!existsSync(pkgPath)) return [];
|
|
42
|
+
let pkg = {};
|
|
43
|
+
try {
|
|
44
|
+
pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const allDeps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
|
|
49
|
+
const suggested = /* @__PURE__ */ new Set();
|
|
50
|
+
for (const rule of SKILL_RULES) {
|
|
51
|
+
if (rule.deps && rule.deps.some((d) => allDeps.includes(d))) {
|
|
52
|
+
rule.skills.forEach((s) => suggested.add(s));
|
|
53
|
+
}
|
|
54
|
+
if (rule.files) {
|
|
55
|
+
const matched = rule.files.some((f) => existsSync(join(projectRoot, f)));
|
|
56
|
+
if (matched) rule.skills.forEach((s) => suggested.add(s));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return [...suggested];
|
|
60
|
+
}
|
|
20
61
|
async function preload(cwd) {
|
|
21
62
|
if (!existsSync(MEMORY_DB_PATH)) {
|
|
22
63
|
log.debug("No memory DB found, skipping preload");
|
|
@@ -66,6 +107,12 @@ async function preload(cwd) {
|
|
|
66
107
|
} finally {
|
|
67
108
|
db.close();
|
|
68
109
|
}
|
|
110
|
+
if (project?.path) {
|
|
111
|
+
const skills = detectRelevantSkills(project.path);
|
|
112
|
+
if (skills.length > 0) {
|
|
113
|
+
sections.push(`Skills: ${skills.join(", ")}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
69
116
|
const output = sections.join("\n");
|
|
70
117
|
log.info(`Preloaded ${sections.length} sections for ${project?.name || cwd} (${output.length} chars)`);
|
|
71
118
|
if (output.length > MAX_TOKENS * 4) {
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-design
|
|
3
|
+
description: REST API design. Enforces consistent endpoints, response envelopes, pagination, error codes, and versioning. Use before building any new endpoint or reviewing an existing API surface.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# API Design
|
|
8
|
+
|
|
9
|
+
Consistency is the only thing that matters in an API. One inconsistent endpoint breaks trust in all of them.
|
|
10
|
+
|
|
11
|
+
## URL conventions
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
GET /api/v1/users # list (always paginated)
|
|
15
|
+
GET /api/v1/users/:id # single resource
|
|
16
|
+
POST /api/v1/users # create
|
|
17
|
+
PATCH /api/v1/users/:id # partial update
|
|
18
|
+
DELETE /api/v1/users/:id # delete
|
|
19
|
+
|
|
20
|
+
# Nested resources (max 2 levels deep)
|
|
21
|
+
GET /api/v1/users/:id/orders
|
|
22
|
+
POST /api/v1/users/:id/orders
|
|
23
|
+
|
|
24
|
+
# Actions (when REST doesn't fit)
|
|
25
|
+
POST /api/v1/users/:id/activate
|
|
26
|
+
POST /api/v1/invoices/:id/send
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- Always plural nouns, never verbs in URL
|
|
30
|
+
- kebab-case for multi-word: `/order-items` not `/orderItems`
|
|
31
|
+
- Version prefix: `/api/v1/` — bump to v2 only for breaking changes
|
|
32
|
+
|
|
33
|
+
## Response envelope
|
|
34
|
+
|
|
35
|
+
**Every response** uses this shape:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// Success
|
|
39
|
+
{
|
|
40
|
+
"data": { ... } | [...],
|
|
41
|
+
"meta": {
|
|
42
|
+
"total": 150, // always on lists
|
|
43
|
+
"limit": 20,
|
|
44
|
+
"offset": 0
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Error
|
|
49
|
+
{
|
|
50
|
+
"data": null,
|
|
51
|
+
"error": {
|
|
52
|
+
"code": "VALIDATION_ERROR", // machine-readable, SCREAMING_SNAKE
|
|
53
|
+
"message": "Email is required", // human-readable, end-user safe
|
|
54
|
+
"field": "email" // optional, for field-level errors
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Never return raw arrays at the top level. Never return different shapes for the same endpoint.
|
|
60
|
+
|
|
61
|
+
## Pagination (mandatory on all lists)
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
GET /api/v1/orders?limit=20&offset=0
|
|
65
|
+
GET /api/v1/orders?limit=20&offset=20
|
|
66
|
+
|
|
67
|
+
// Response
|
|
68
|
+
{
|
|
69
|
+
"data": [...],
|
|
70
|
+
"meta": { "total": 847, "limit": 20, "offset": 0 }
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- Default limit: 20. Max limit: 100 (enforce server-side).
|
|
75
|
+
- Never return unbounded lists. Always paginate.
|
|
76
|
+
- Frontend shows `total` from meta, not `data.length`.
|
|
77
|
+
|
|
78
|
+
## HTTP status codes
|
|
79
|
+
|
|
80
|
+
| Code | When |
|
|
81
|
+
|------|------|
|
|
82
|
+
| 200 | Success (GET, PATCH) |
|
|
83
|
+
| 201 | Created (POST) — include `Location` header |
|
|
84
|
+
| 204 | Deleted (DELETE) — no body |
|
|
85
|
+
| 400 | Bad request (malformed JSON, invalid params) |
|
|
86
|
+
| 401 | Not authenticated (missing/expired token) |
|
|
87
|
+
| 403 | Authenticated but not authorized |
|
|
88
|
+
| 404 | Resource not found |
|
|
89
|
+
| 409 | Conflict (duplicate email, concurrent update) |
|
|
90
|
+
| 422 | Validation error (valid JSON but business rule violated) |
|
|
91
|
+
| 429 | Rate limited — include `Retry-After` header |
|
|
92
|
+
| 500 | Server error — log internally, never expose stack trace |
|
|
93
|
+
|
|
94
|
+
## Error codes
|
|
95
|
+
|
|
96
|
+
Use machine-readable `code` values the frontend can switch on:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
VALIDATION_ERROR — field validation failed
|
|
100
|
+
NOT_FOUND — resource doesn't exist
|
|
101
|
+
UNAUTHORIZED — not logged in
|
|
102
|
+
FORBIDDEN — logged in but no permission
|
|
103
|
+
DUPLICATE — unique constraint violation
|
|
104
|
+
RATE_LIMITED — too many requests
|
|
105
|
+
INTERNAL_ERROR — catch-all for 500s
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Validation
|
|
109
|
+
|
|
110
|
+
- Validate at the boundary — never trust client data inside business logic
|
|
111
|
+
- Return ALL validation errors at once, not one by one
|
|
112
|
+
- Use `422` + `field` in error for form validation, `400` for structural issues
|
|
113
|
+
|
|
114
|
+
## Rate limiting headers
|
|
115
|
+
|
|
116
|
+
Always include on authenticated endpoints:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
X-RateLimit-Limit: 100
|
|
120
|
+
X-RateLimit-Remaining: 42
|
|
121
|
+
X-RateLimit-Reset: 1735689600 # Unix timestamp
|
|
122
|
+
Retry-After: 30 # seconds, on 429
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Checklist before shipping an endpoint
|
|
126
|
+
|
|
127
|
+
- [ ] URL follows conventions (plural, kebab, versioned)
|
|
128
|
+
- [ ] Response uses the standard envelope
|
|
129
|
+
- [ ] List endpoint is paginated (limit+offset+total)
|
|
130
|
+
- [ ] All error cases return correct status + error code
|
|
131
|
+
- [ ] Validation errors return field-level details
|
|
132
|
+
- [ ] No secrets or internal paths in responses
|
|
133
|
+
- [ ] Auth required where needed (don't forget!)
|
|
134
|
+
- [ ] Rate limiting on sensitive endpoints (auth, email send)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: auth-patterns
|
|
3
|
+
description: Authentication and authorization patterns. JWT, sessions, OAuth, RBAC, route protection. Use when implementing login, protected routes, or permission systems.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Auth Patterns
|
|
8
|
+
|
|
9
|
+
Auth bugs are security bugs. Every decision here has a security consequence.
|
|
10
|
+
|
|
11
|
+
## Token storage
|
|
12
|
+
|
|
13
|
+
| Method | XSS safe | CSRF safe | Use for |
|
|
14
|
+
|--------|----------|-----------|---------|
|
|
15
|
+
| `httpOnly` cookie | ✅ | ❌ (need CSRF token) | Sessions, refresh tokens |
|
|
16
|
+
| Memory (React state) | ✅ | ✅ | Access tokens (short-lived) |
|
|
17
|
+
| `localStorage` | ❌ | ✅ | Never for auth tokens |
|
|
18
|
+
| `sessionStorage` | ❌ | ✅ | Never for auth tokens |
|
|
19
|
+
|
|
20
|
+
**Rule:** Access token in memory. Refresh token in `httpOnly` `Secure` `SameSite=Strict` cookie.
|
|
21
|
+
|
|
22
|
+
## JWT structure
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// Access token: short-lived, stateless
|
|
26
|
+
const accessToken = jwt.sign(
|
|
27
|
+
{ sub: user.id, role: user.role, email: user.email },
|
|
28
|
+
ACCESS_TOKEN_SECRET,
|
|
29
|
+
{ expiresIn: '15m' } // never more than 1h
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// Refresh token: long-lived, stored in DB for revocation
|
|
33
|
+
const refreshToken = jwt.sign(
|
|
34
|
+
{ sub: user.id, jti: crypto.randomUUID() }, // jti = unique ID for revocation
|
|
35
|
+
REFRESH_TOKEN_SECRET,
|
|
36
|
+
{ expiresIn: '30d' }
|
|
37
|
+
)
|
|
38
|
+
// Store hashed refresh token in DB
|
|
39
|
+
await db.refreshTokens.insert({ userId: user.id, tokenHash: hash(refreshToken) })
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Never:** embed sensitive data in JWT (passwords, full profile, card numbers).
|
|
43
|
+
**Always:** verify on every request, don't trust payload without signature check.
|
|
44
|
+
|
|
45
|
+
## Route protection (Next.js App Router)
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// middleware.ts — runs on every request before page render
|
|
49
|
+
import { NextResponse } from 'next/server'
|
|
50
|
+
import { verifyToken } from '@/lib/auth'
|
|
51
|
+
|
|
52
|
+
export async function middleware(request: NextRequest) {
|
|
53
|
+
const token = request.cookies.get('access_token')?.value
|
|
54
|
+
|
|
55
|
+
if (!token) {
|
|
56
|
+
return NextResponse.redirect(new URL('/login', request.url))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const payload = await verifyToken(token)
|
|
60
|
+
if (!payload) {
|
|
61
|
+
return NextResponse.redirect(new URL('/login', request.url))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return NextResponse.next()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const config = {
|
|
68
|
+
matcher: ['/dashboard/:path*', '/settings/:path*', '/api/v1/:path*'],
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Never** protect routes only client-side — always enforce on server/middleware.
|
|
73
|
+
|
|
74
|
+
## RBAC (Role-Based Access Control)
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// Define roles and permissions clearly
|
|
78
|
+
const PERMISSIONS = {
|
|
79
|
+
'admin': ['read', 'write', 'delete', 'manage_users'],
|
|
80
|
+
'editor': ['read', 'write'],
|
|
81
|
+
'viewer': ['read'],
|
|
82
|
+
} as const
|
|
83
|
+
|
|
84
|
+
// Check permission, not role (more flexible)
|
|
85
|
+
function can(user: User, action: string): boolean {
|
|
86
|
+
return PERMISSIONS[user.role]?.includes(action) ?? false
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Usage
|
|
90
|
+
if (!can(user, 'delete')) {
|
|
91
|
+
return res.status(403).json({ error: { code: 'FORBIDDEN', message: 'Insufficient permissions' } })
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Never:** check `user.role === 'admin'` inline everywhere. Centralize permission logic.
|
|
96
|
+
|
|
97
|
+
## Password handling
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import bcrypt from 'bcrypt' // or argon2
|
|
101
|
+
|
|
102
|
+
// Hash on registration (never store plain text)
|
|
103
|
+
const hash = await bcrypt.hash(password, 12) // cost factor 12
|
|
104
|
+
|
|
105
|
+
// Verify on login
|
|
106
|
+
const valid = await bcrypt.compare(password, user.passwordHash)
|
|
107
|
+
|
|
108
|
+
// Timing-safe comparison for tokens
|
|
109
|
+
import { timingSafeEqual } from 'node:crypto'
|
|
110
|
+
const match = timingSafeEqual(Buffer.from(token), Buffer.from(expectedToken))
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## OAuth (social login)
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// Always use a library — don't implement OAuth yourself
|
|
117
|
+
// next-auth / auth.js for Next.js
|
|
118
|
+
// passport.js for Express
|
|
119
|
+
|
|
120
|
+
// Validate state parameter to prevent CSRF
|
|
121
|
+
// Validate redirect_uri server-side
|
|
122
|
+
// Store minimal profile data (don't keep what you don't need)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Rate limiting on auth endpoints
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// Login: 5 attempts per 15min per IP
|
|
129
|
+
// Password reset: 3 per hour per email
|
|
130
|
+
// Token refresh: 10 per minute per token
|
|
131
|
+
// Registration: 3 per hour per IP
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Checklist
|
|
135
|
+
|
|
136
|
+
- [ ] Access tokens expire in ≤1h
|
|
137
|
+
- [ ] Refresh tokens stored as hash in DB (revocable)
|
|
138
|
+
- [ ] Tokens in `httpOnly` cookies, never `localStorage`
|
|
139
|
+
- [ ] Passwords hashed with bcrypt/argon2 (cost ≥12)
|
|
140
|
+
- [ ] CSRF protection on all cookie-based state changes
|
|
141
|
+
- [ ] Rate limiting on login, register, reset endpoints
|
|
142
|
+
- [ ] All protected routes verified server-side (not just client guard)
|
|
143
|
+
- [ ] `HTTPS` only in production (`Secure` cookie flag)
|
|
144
|
+
- [ ] Logout invalidates refresh token in DB
|
|
145
|
+
- [ ] No sensitive data in JWT payload
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: db-design
|
|
3
|
+
description: Database schema design, indexing strategy, and migration patterns. Prevents N+1, missing indexes, unsafe migrations, and schema drift. Use when designing tables, adding columns, or writing complex queries.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# DB Design
|
|
8
|
+
|
|
9
|
+
Bad schema is the most expensive technical debt. You can refactor code in hours; migrating 10M rows takes days.
|
|
10
|
+
|
|
11
|
+
## Schema design principles
|
|
12
|
+
|
|
13
|
+
### Naming
|
|
14
|
+
- Tables: `snake_case`, plural (`users`, `order_items`, `refresh_tokens`)
|
|
15
|
+
- Columns: `snake_case` (`created_at`, `user_id`, `is_active`)
|
|
16
|
+
- Foreign keys: `{table_singular}_id` (`user_id`, `order_id`)
|
|
17
|
+
- Booleans: prefix `is_` or `has_` (`is_active`, `has_verified_email`)
|
|
18
|
+
- Timestamps: always `created_at` + `updated_at` on every table
|
|
19
|
+
|
|
20
|
+
### Standard columns every table needs
|
|
21
|
+
```sql
|
|
22
|
+
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY -- or UUID
|
|
23
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
24
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Soft delete pattern (prefer over hard delete)
|
|
28
|
+
```sql
|
|
29
|
+
deleted_at TIMESTAMPTZ -- NULL = active, non-NULL = deleted
|
|
30
|
+
-- Query: WHERE deleted_at IS NULL
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Indexing strategy
|
|
34
|
+
|
|
35
|
+
**Index everything you filter, sort, or join on.**
|
|
36
|
+
|
|
37
|
+
```sql
|
|
38
|
+
-- Always index foreign keys
|
|
39
|
+
CREATE INDEX idx_orders_user_id ON orders(user_id);
|
|
40
|
+
|
|
41
|
+
-- Composite index for common query patterns (leftmost prefix rule)
|
|
42
|
+
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
|
|
43
|
+
-- This covers: WHERE user_id = ?
|
|
44
|
+
-- And: WHERE user_id = ? AND status = ?
|
|
45
|
+
-- But NOT: WHERE status = ? alone
|
|
46
|
+
|
|
47
|
+
-- Partial index for filtered queries
|
|
48
|
+
CREATE INDEX idx_orders_pending ON orders(created_at)
|
|
49
|
+
WHERE status = 'pending';
|
|
50
|
+
|
|
51
|
+
-- Text search
|
|
52
|
+
CREATE INDEX idx_products_name_search ON products USING gin(to_tsvector('english', name));
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Check for missing indexes:**
|
|
56
|
+
```sql
|
|
57
|
+
EXPLAIN ANALYZE SELECT ... -- look for "Seq Scan" on large tables
|
|
58
|
+
-- Seq Scan on 1000 rows = fine. On 1M rows = add an index.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Over-indexing costs:** Every index slows down INSERT/UPDATE/DELETE. Don't index columns you never filter on.
|
|
62
|
+
|
|
63
|
+
## N+1 pattern (the most common DB bug)
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// BAD — N+1: 1 query for orders + N queries for each user
|
|
67
|
+
const orders = await db.orders.findMany()
|
|
68
|
+
for (const order of orders) {
|
|
69
|
+
order.user = await db.users.findById(order.userId) // N queries!
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// GOOD — 1 query with join or eager load
|
|
73
|
+
const orders = await db.orders.findMany({
|
|
74
|
+
include: { user: true } // Prisma
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Or raw SQL with JOIN
|
|
78
|
+
SELECT o.*, u.name, u.email
|
|
79
|
+
FROM orders o
|
|
80
|
+
JOIN users u ON u.id = o.user_id
|
|
81
|
+
WHERE o.status = 'pending'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Safe migration patterns
|
|
85
|
+
|
|
86
|
+
```sql
|
|
87
|
+
-- SAFE: adding nullable column (instant, no lock)
|
|
88
|
+
ALTER TABLE users ADD COLUMN avatar_url TEXT;
|
|
89
|
+
|
|
90
|
+
-- SAFE: adding column with default (Postgres 11+, instant)
|
|
91
|
+
ALTER TABLE users ADD COLUMN is_verified BOOLEAN NOT NULL DEFAULT false;
|
|
92
|
+
|
|
93
|
+
-- DANGEROUS: adding NOT NULL without default (locks table, blocks writes)
|
|
94
|
+
-- Do it in 3 steps:
|
|
95
|
+
-- 1. Add nullable
|
|
96
|
+
ALTER TABLE users ADD COLUMN phone TEXT;
|
|
97
|
+
-- 2. Backfill
|
|
98
|
+
UPDATE users SET phone = '' WHERE phone IS NULL;
|
|
99
|
+
-- 3. Add constraint
|
|
100
|
+
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
|
|
101
|
+
|
|
102
|
+
-- DANGEROUS: dropping column (data loss, code must be deployed first)
|
|
103
|
+
-- 1. Deploy code that no longer uses the column
|
|
104
|
+
-- 2. Then drop in next migration
|
|
105
|
+
ALTER TABLE users DROP COLUMN legacy_field;
|
|
106
|
+
|
|
107
|
+
-- DANGEROUS: renaming column/table (breaks existing queries)
|
|
108
|
+
-- Use a 2-phase rename: add new column, migrate data, remove old
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Transaction patterns
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// Always use transactions for multi-step operations
|
|
115
|
+
await db.transaction(async (tx) => {
|
|
116
|
+
const order = await tx.orders.create({ data: { userId, total } })
|
|
117
|
+
await tx.inventory.decrement({ productId, quantity })
|
|
118
|
+
await tx.payments.create({ data: { orderId: order.id, amount: total } })
|
|
119
|
+
// If any step fails, all are rolled back
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Query hygiene
|
|
124
|
+
|
|
125
|
+
- Never `SELECT *` in production — select only what you need
|
|
126
|
+
- Parameterized queries always (no string interpolation)
|
|
127
|
+
- `LIMIT` on all queries that could return unbounded results
|
|
128
|
+
- Avoid `OFFSET` for large pagination → use cursor-based (`WHERE id > lastId`)
|
|
129
|
+
|
|
130
|
+
## Checklist
|
|
131
|
+
|
|
132
|
+
- [ ] Every table has `id`, `created_at`, `updated_at`
|
|
133
|
+
- [ ] All foreign keys have indexes
|
|
134
|
+
- [ ] Columns filtered/sorted in queries have indexes
|
|
135
|
+
- [ ] `EXPLAIN ANALYZE` run on queries touching >10k rows
|
|
136
|
+
- [ ] Migrations are reversible (or have a rollback plan)
|
|
137
|
+
- [ ] No `ALTER TABLE` without reviewing lock implications
|
|
138
|
+
- [ ] Multi-step operations wrapped in transactions
|
|
139
|
+
- [ ] No `SELECT *`, no string-interpolated queries
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: error-handling
|
|
3
|
+
description: Error handling strategy. Error boundaries, logging patterns, user-facing messages, monitoring setup, and async error flows. Use when building any feature that can fail, or auditing error handling in existing code.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Error Handling
|
|
8
|
+
|
|
9
|
+
Every error falls into two buckets: expected (handle gracefully) and unexpected (log, alert, recover).
|
|
10
|
+
Never show users a stack trace. Never silently swallow an error.
|
|
11
|
+
|
|
12
|
+
## Error hierarchy
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Expected errors (handle in code)
|
|
16
|
+
├── Validation errors → show to user, guide to fix
|
|
17
|
+
├── Not found → 404, redirect or empty state
|
|
18
|
+
├── Auth errors → redirect to login
|
|
19
|
+
├── Business rule violations → explain and offer alternative
|
|
20
|
+
└── External API failures → retry or fallback
|
|
21
|
+
|
|
22
|
+
Unexpected errors (log + alert + recover)
|
|
23
|
+
├── Unhandled promise rejections
|
|
24
|
+
├── Database connection failures
|
|
25
|
+
├── Third-party SDK crashes
|
|
26
|
+
└── Memory/timeout errors
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Frontend: React Error Boundaries
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
// components/ErrorBoundary.tsx
|
|
33
|
+
'use client'
|
|
34
|
+
import { Component, ReactNode } from 'react'
|
|
35
|
+
|
|
36
|
+
interface Props { children: ReactNode; fallback?: ReactNode }
|
|
37
|
+
interface State { hasError: boolean; error?: Error }
|
|
38
|
+
|
|
39
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
40
|
+
state: State = { hasError: false }
|
|
41
|
+
|
|
42
|
+
static getDerivedStateFromError(error: Error): State {
|
|
43
|
+
return { hasError: true, error }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
componentDidCatch(error: Error, info: { componentStack: string }) {
|
|
47
|
+
// Log to your monitoring service
|
|
48
|
+
console.error('[ErrorBoundary]', error, info)
|
|
49
|
+
reportError(error, { context: info.componentStack })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
render() {
|
|
53
|
+
if (this.state.hasError) {
|
|
54
|
+
return this.props.fallback ?? <DefaultErrorFallback error={this.state.error} />
|
|
55
|
+
}
|
|
56
|
+
return this.props.children
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Wrap at route level and around risky components
|
|
61
|
+
<ErrorBoundary fallback={<ErrorPage />}>
|
|
62
|
+
<Dashboard />
|
|
63
|
+
</ErrorBoundary>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Frontend: async error patterns
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
// NEVER: unhandled promise
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
fetchData() // if this throws, silent failure
|
|
72
|
+
}, [])
|
|
73
|
+
|
|
74
|
+
// GOOD: handle errors explicitly
|
|
75
|
+
const [error, setError] = useState<string | null>(null)
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
fetchData()
|
|
79
|
+
.catch(err => {
|
|
80
|
+
setError(getErrorMessage(err))
|
|
81
|
+
reportError(err)
|
|
82
|
+
})
|
|
83
|
+
}, [])
|
|
84
|
+
|
|
85
|
+
// GOOD: React Query handles this automatically
|
|
86
|
+
const { data, error, isError } = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
|
|
87
|
+
if (isError) return <ErrorState message={getErrorMessage(error)} />
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Next.js App Router error files
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// app/error.tsx — catches runtime errors in route segment
|
|
94
|
+
'use client'
|
|
95
|
+
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
|
96
|
+
useEffect(() => { reportError(error) }, [error])
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div>
|
|
100
|
+
<h2>Something went wrong</h2>
|
|
101
|
+
<button onClick={reset}>Try again</button>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// app/not-found.tsx — 404 handler
|
|
107
|
+
export default function NotFound() {
|
|
108
|
+
return <div>Page not found</div>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// app/global-error.tsx — catches errors in root layout
|
|
112
|
+
'use client'
|
|
113
|
+
export default function GlobalError({ error, reset }) { ... }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Backend: error response pattern
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// Never expose internal errors to clients
|
|
120
|
+
class AppError extends Error {
|
|
121
|
+
constructor(
|
|
122
|
+
public code: string, // machine-readable
|
|
123
|
+
public message: string, // end-user safe
|
|
124
|
+
public status: number, // HTTP status
|
|
125
|
+
public details?: unknown // optional context (logged, not returned)
|
|
126
|
+
) { super(message) }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Express global error handler
|
|
130
|
+
app.use((err: Error, req, res, next) => {
|
|
131
|
+
if (err instanceof AppError) {
|
|
132
|
+
return res.status(err.status).json({
|
|
133
|
+
data: null,
|
|
134
|
+
error: { code: err.code, message: err.message }
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Unexpected error — log full details, return generic message
|
|
139
|
+
logger.error({ err, req: { method: req.method, url: req.url } })
|
|
140
|
+
reportError(err)
|
|
141
|
+
|
|
142
|
+
res.status(500).json({
|
|
143
|
+
data: null,
|
|
144
|
+
error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' }
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Logging levels
|
|
150
|
+
|
|
151
|
+
| Level | When | Example |
|
|
152
|
+
|-------|------|---------|
|
|
153
|
+
| `error` | Unexpected failures, alerts needed | DB crash, unhandled exception |
|
|
154
|
+
| `warn` | Expected failures worth monitoring | Retry attempts, rate limit hit |
|
|
155
|
+
| `info` | Key business events | User registered, payment processed |
|
|
156
|
+
| `debug` | Dev only, never in production | Query params, response body |
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// Always log with context, not just the message
|
|
160
|
+
logger.error({ err, userId: req.user?.id, endpoint: req.url }, 'Payment failed')
|
|
161
|
+
// Not: logger.error('Payment failed')
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Async error rules
|
|
165
|
+
|
|
166
|
+
- Every `async` function must have a `try/catch` or be called with `.catch()`
|
|
167
|
+
- Never `await` inside `forEach` — use `Promise.all` or `for...of`
|
|
168
|
+
- Unhandled promise rejections crash Node.js 15+ — always handle
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// GOOD
|
|
172
|
+
const results = await Promise.all(items.map(item => processItem(item)))
|
|
173
|
+
|
|
174
|
+
// BAD (forEach ignores returned promise)
|
|
175
|
+
items.forEach(async (item) => await processItem(item))
|
|
176
|
+
|
|
177
|
+
// For sequential with error isolation:
|
|
178
|
+
for (const item of items) {
|
|
179
|
+
try {
|
|
180
|
+
await processItem(item)
|
|
181
|
+
} catch (err) {
|
|
182
|
+
logger.warn({ err, itemId: item.id }, 'Failed to process item, continuing')
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Checklist
|
|
188
|
+
|
|
189
|
+
- [ ] Error boundaries around route segments and risky components
|
|
190
|
+
- [ ] Every `fetch`/`async` call has error handling
|
|
191
|
+
- [ ] User sees human-readable message, never stack trace or raw error
|
|
192
|
+
- [ ] Unexpected errors logged with full context (user, endpoint, stack)
|
|
193
|
+
- [ ] `app/error.tsx` and `app/not-found.tsx` exist and are styled
|
|
194
|
+
- [ ] Global error handler in API (Express/Hono/Next route handler)
|
|
195
|
+
- [ ] `AppError` class for expected errors with machine-readable codes
|
|
196
|
+
- [ ] Error monitoring service wired up (see monitoring patterns)
|