rex-claude 6.1.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/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)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: i18n
|
|
3
|
+
description: Internationalization patterns for Next.js. next-intl setup, message extraction, locale routing, date/number formatting, and pluralization. Use when adding multi-language support or auditing an existing i18n setup.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# i18n (Next.js + next-intl)
|
|
8
|
+
|
|
9
|
+
i18n added after launch is painful. i18n from day one is just a folder structure.
|
|
10
|
+
|
|
11
|
+
## Setup (next-intl)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install next-intl
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
messages/
|
|
19
|
+
├── en.json
|
|
20
|
+
├── fr.json
|
|
21
|
+
└── es.json
|
|
22
|
+
|
|
23
|
+
app/
|
|
24
|
+
├── [locale]/
|
|
25
|
+
│ ├── layout.tsx
|
|
26
|
+
│ └── page.tsx
|
|
27
|
+
├── i18n/
|
|
28
|
+
│ ├── routing.ts
|
|
29
|
+
│ └── request.ts
|
|
30
|
+
└── middleware.ts
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// i18n/routing.ts
|
|
35
|
+
import { defineRouting } from 'next-intl/routing'
|
|
36
|
+
|
|
37
|
+
export const routing = defineRouting({
|
|
38
|
+
locales: ['en', 'fr', 'es'],
|
|
39
|
+
defaultLocale: 'en',
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// middleware.ts
|
|
43
|
+
import createMiddleware from 'next-intl/middleware'
|
|
44
|
+
import { routing } from './i18n/routing'
|
|
45
|
+
|
|
46
|
+
export default createMiddleware(routing)
|
|
47
|
+
|
|
48
|
+
export const config = {
|
|
49
|
+
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'],
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Message files
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
// messages/en.json
|
|
57
|
+
{
|
|
58
|
+
"nav": {
|
|
59
|
+
"home": "Home",
|
|
60
|
+
"pricing": "Pricing",
|
|
61
|
+
"signin": "Sign in"
|
|
62
|
+
},
|
|
63
|
+
"dashboard": {
|
|
64
|
+
"welcome": "Welcome back, {name}!",
|
|
65
|
+
"items": "{count, plural, =0 {No items} one {# item} other {# items}}",
|
|
66
|
+
"lastSeen": "Last seen {date, relativetime}"
|
|
67
|
+
},
|
|
68
|
+
"errors": {
|
|
69
|
+
"required": "{field} is required",
|
|
70
|
+
"notFound": "Page not found"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Usage in components
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
// Server component
|
|
79
|
+
import { getTranslations } from 'next-intl/server'
|
|
80
|
+
|
|
81
|
+
export default async function Page() {
|
|
82
|
+
const t = await getTranslations('dashboard')
|
|
83
|
+
return <h1>{t('welcome', { name: 'Kevin' })}</h1>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Client component
|
|
87
|
+
'use client'
|
|
88
|
+
import { useTranslations } from 'next-intl'
|
|
89
|
+
|
|
90
|
+
export function NavBar() {
|
|
91
|
+
const t = useTranslations('nav')
|
|
92
|
+
return <nav><a>{t('home')}</a></nav>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Pluralization
|
|
96
|
+
t('items', { count: 0 }) // "No items"
|
|
97
|
+
t('items', { count: 1 }) // "1 item"
|
|
98
|
+
t('items', { count: 42 }) // "42 items"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Date, number, and currency formatting
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { useFormatter } from 'next-intl'
|
|
105
|
+
|
|
106
|
+
export function PricingCard({ price, date }: { price: number; date: Date }) {
|
|
107
|
+
const format = useFormatter()
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div>
|
|
111
|
+
{/* Currency — auto-formats per locale */}
|
|
112
|
+
<p>{format.number(price, { style: 'currency', currency: 'EUR' })}</p>
|
|
113
|
+
{/* fr: 29,99 € en: €29.99 */}
|
|
114
|
+
|
|
115
|
+
{/* Relative time */}
|
|
116
|
+
<time>{format.relativeTime(date)}</time>
|
|
117
|
+
{/* "3 hours ago" / "il y a 3 heures" */}
|
|
118
|
+
|
|
119
|
+
{/* Absolute date */}
|
|
120
|
+
<span>{format.dateTime(date, { dateStyle: 'long' })}</span>
|
|
121
|
+
{/* "March 7, 2026" / "7 mars 2026" */}
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Locale-aware routing
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import { Link } from '@/i18n/routing' // next-intl's Link (adds locale prefix)
|
|
131
|
+
|
|
132
|
+
// /en/dashboard → /fr/dashboard automatically
|
|
133
|
+
<Link href="/dashboard">Dashboard</Link>
|
|
134
|
+
|
|
135
|
+
// Redirect to locale-specific path
|
|
136
|
+
import { redirect } from 'next-intl/navigation'
|
|
137
|
+
redirect({ href: '/login', locale: 'fr' })
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Common mistakes to avoid
|
|
141
|
+
|
|
142
|
+
| Don't | Do instead |
|
|
143
|
+
|-------|-----------|
|
|
144
|
+
| Hardcode strings in JSX | Extract to message files from day 1 |
|
|
145
|
+
| Use `new Date().toLocaleDateString()` | Use `format.dateTime()` |
|
|
146
|
+
| Build plural strings manually (`${count} item(s)`) | Use ICU plural syntax in messages |
|
|
147
|
+
| Store locale in state | Use URL-based routing (SEO + shareable) |
|
|
148
|
+
| Forget RTL support | Add `dir={locale === 'ar' ? 'rtl' : 'ltr'}` to `<html>` |
|
|
149
|
+
|
|
150
|
+
## Checklist
|
|
151
|
+
|
|
152
|
+
- [ ] Locale detected from URL, not browser/cookie (SEO friendly)
|
|
153
|
+
- [ ] All user-facing strings in message files (grep for hardcoded text)
|
|
154
|
+
- [ ] Dates formatted with `format.dateTime()`, numbers with `format.number()`
|
|
155
|
+
- [ ] Plurals use ICU syntax, not manual string building
|
|
156
|
+
- [ ] `<Link>` from next-intl (not next/link) for locale-aware navigation
|
|
157
|
+
- [ ] `<html lang={locale}>` set in root layout
|
|
158
|
+
- [ ] Fallback to `defaultLocale` when translation missing
|
|
159
|
+
- [ ] OG metadata localized per language
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: perf
|
|
3
|
+
description: Performance audit. Identifies bottlenecks in frontend (Core Web Vitals, bundle, rendering), backend (slow queries, N+1, missing indexes), and infrastructure (caching, CDN). Reports with file:line and concrete fixes. Use when the app feels slow or before a launch.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Performance
|
|
8
|
+
|
|
9
|
+
Measure first. Never guess where the bottleneck is.
|
|
10
|
+
|
|
11
|
+
## Frontend
|
|
12
|
+
|
|
13
|
+
### Core Web Vitals targets
|
|
14
|
+
| Metric | Good | Needs work |
|
|
15
|
+
|--------|------|-----------|
|
|
16
|
+
| LCP (largest content) | <2.5s | >4s |
|
|
17
|
+
| INP (interaction) | <200ms | >500ms |
|
|
18
|
+
| CLS (layout shift) | <0.1 | >0.25 |
|
|
19
|
+
|
|
20
|
+
### Bundle audit
|
|
21
|
+
```bash
|
|
22
|
+
# Next.js
|
|
23
|
+
ANALYZE=true next build # needs @next/bundle-analyzer
|
|
24
|
+
|
|
25
|
+
# Check for duplicate deps
|
|
26
|
+
npx depcheck
|
|
27
|
+
npx bundlephobia <package> # before adding anything
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Red flags:**
|
|
31
|
+
- `moment.js` (replace with `date-fns` or `dayjs`)
|
|
32
|
+
- Full `lodash` import (use `lodash-es` + tree-shaking)
|
|
33
|
+
- Unoptimized images (no `next/image`, missing `width`/`height`)
|
|
34
|
+
- No code splitting (all JS in one chunk)
|
|
35
|
+
- `useEffect` on every render with no dep array
|
|
36
|
+
|
|
37
|
+
### Rendering bottlenecks
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// BAD: expensive filter on every render
|
|
41
|
+
const filtered = items.filter(...)
|
|
42
|
+
|
|
43
|
+
// GOOD: memoized
|
|
44
|
+
const filtered = useMemo(() => items.filter(...), [items, query])
|
|
45
|
+
|
|
46
|
+
// BAD: new function reference breaks child memo
|
|
47
|
+
<Child onClick={() => doSomething()} />
|
|
48
|
+
|
|
49
|
+
// GOOD
|
|
50
|
+
const handleClick = useCallback(() => doSomething(), [])
|
|
51
|
+
<Child onClick={handleClick} />
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Check:**
|
|
55
|
+
- [ ] Lists with 100+ items: use virtualization (`react-window` or `@tanstack/virtual`)
|
|
56
|
+
- [ ] Heavy components: lazy load with `dynamic(() => import(...), { ssr: false })`
|
|
57
|
+
- [ ] Images: `next/image` with `priority` on above-the-fold, lazy below
|
|
58
|
+
- [ ] Fonts: `next/font` only, never `<link>` to Google Fonts
|
|
59
|
+
|
|
60
|
+
## Backend / API
|
|
61
|
+
|
|
62
|
+
### Query audit
|
|
63
|
+
|
|
64
|
+
```sql
|
|
65
|
+
-- Spot slow queries (Postgres)
|
|
66
|
+
SELECT query, mean_exec_time, calls
|
|
67
|
+
FROM pg_stat_statements
|
|
68
|
+
ORDER BY mean_exec_time DESC LIMIT 20;
|
|
69
|
+
|
|
70
|
+
-- Missing indexes
|
|
71
|
+
EXPLAIN ANALYZE SELECT ...; -- look for "Seq Scan" on large tables
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Red flags:**
|
|
75
|
+
- `SELECT *` on wide tables
|
|
76
|
+
- Missing indexes on foreign keys and filtered columns
|
|
77
|
+
- N+1: loop calling DB inside `.map()` — batch with `WHERE id IN (...)` instead
|
|
78
|
+
- No pagination on list endpoints
|
|
79
|
+
- Sorting on non-indexed column
|
|
80
|
+
|
|
81
|
+
### Caching strategy
|
|
82
|
+
|
|
83
|
+
| Layer | Tool | TTL |
|
|
84
|
+
|-------|------|-----|
|
|
85
|
+
| Static data | CDN edge cache | 1h–24h |
|
|
86
|
+
| API responses | Redis / Cloudflare KV | 1min–1h |
|
|
87
|
+
| DB queries | In-memory LRU | 30s |
|
|
88
|
+
| Images | `Cache-Control: public, max-age=31536000` | 1 year |
|
|
89
|
+
|
|
90
|
+
## Audit output format
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
## Perf Audit — [app/page]
|
|
94
|
+
|
|
95
|
+
### Critical (>1s impact)
|
|
96
|
+
- [file:line] Description + fix
|
|
97
|
+
|
|
98
|
+
### High (100ms–1s)
|
|
99
|
+
- ...
|
|
100
|
+
|
|
101
|
+
### Quick wins (<1h to fix)
|
|
102
|
+
- ...
|
|
103
|
+
|
|
104
|
+
### Metrics baseline
|
|
105
|
+
- Bundle: Xkb (main), Ykb (page)
|
|
106
|
+
- TTFB: Xms
|
|
107
|
+
- LCP: Xs
|
|
108
|
+
```
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: seo
|
|
3
|
+
description: SEO implementation for Next.js. Metadata, OG tags, sitemap, robots.txt, structured data, and Core Web Vitals. Use when building landing pages, blog, or any public-facing page.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# SEO (Next.js App Router)
|
|
8
|
+
|
|
9
|
+
SEO is plumbing. Do it right once and forget it. Do it wrong and it's invisible for 6 months.
|
|
10
|
+
|
|
11
|
+
## Metadata API (App Router)
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
// app/layout.tsx — site-wide defaults
|
|
15
|
+
export const metadata: Metadata = {
|
|
16
|
+
metadataBase: new URL('https://yourdomain.com'),
|
|
17
|
+
title: {
|
|
18
|
+
default: 'Your App Name',
|
|
19
|
+
template: '%s | Your App Name', // page title → "Page | Your App Name"
|
|
20
|
+
},
|
|
21
|
+
description: 'Clear, 150-char max description of what the app does.',
|
|
22
|
+
openGraph: {
|
|
23
|
+
type: 'website',
|
|
24
|
+
locale: 'en_US',
|
|
25
|
+
url: 'https://yourdomain.com',
|
|
26
|
+
siteName: 'Your App Name',
|
|
27
|
+
images: [{ url: '/og-default.png', width: 1200, height: 630 }],
|
|
28
|
+
},
|
|
29
|
+
twitter: {
|
|
30
|
+
card: 'summary_large_image',
|
|
31
|
+
creator: '@yourhandle',
|
|
32
|
+
},
|
|
33
|
+
robots: { index: true, follow: true },
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// app/blog/[slug]/page.tsx — dynamic per-page metadata
|
|
37
|
+
export async function generateMetadata({ params }): Promise<Metadata> {
|
|
38
|
+
const post = await getPost(params.slug)
|
|
39
|
+
return {
|
|
40
|
+
title: post.title,
|
|
41
|
+
description: post.excerpt,
|
|
42
|
+
openGraph: {
|
|
43
|
+
type: 'article',
|
|
44
|
+
publishedTime: post.publishedAt,
|
|
45
|
+
images: [{ url: post.ogImage, width: 1200, height: 630 }],
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## OG Image generation
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
// app/og/route.tsx — dynamic OG images (Vercel OG)
|
|
55
|
+
import { ImageResponse } from 'next/og'
|
|
56
|
+
|
|
57
|
+
export async function GET(request: Request) {
|
|
58
|
+
const { searchParams } = new URL(request.url)
|
|
59
|
+
const title = searchParams.get('title') || 'Default Title'
|
|
60
|
+
|
|
61
|
+
return new ImageResponse(
|
|
62
|
+
<div style={{ display: 'flex', width: '100%', height: '100%', background: '#000' }}>
|
|
63
|
+
<h1 style={{ color: '#fff', fontSize: 60 }}>{title}</h1>
|
|
64
|
+
</div>,
|
|
65
|
+
{ width: 1200, height: 630 }
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Use in metadata:
|
|
70
|
+
// images: [{ url: `/og?title=${encodeURIComponent(post.title)}` }]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Sitemap
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
// app/sitemap.ts
|
|
77
|
+
import { MetadataRoute } from 'next'
|
|
78
|
+
|
|
79
|
+
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
80
|
+
const posts = await getAllPosts()
|
|
81
|
+
|
|
82
|
+
return [
|
|
83
|
+
{ url: 'https://yourdomain.com', lastModified: new Date(), changeFrequency: 'weekly', priority: 1 },
|
|
84
|
+
{ url: 'https://yourdomain.com/pricing', lastModified: new Date(), changeFrequency: 'monthly', priority: 0.8 },
|
|
85
|
+
...posts.map(post => ({
|
|
86
|
+
url: `https://yourdomain.com/blog/${post.slug}`,
|
|
87
|
+
lastModified: post.updatedAt,
|
|
88
|
+
changeFrequency: 'monthly' as const,
|
|
89
|
+
priority: 0.6,
|
|
90
|
+
})),
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Robots.txt
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
// app/robots.ts
|
|
99
|
+
import { MetadataRoute } from 'next'
|
|
100
|
+
|
|
101
|
+
export default function robots(): MetadataRoute.Robots {
|
|
102
|
+
return {
|
|
103
|
+
rules: [
|
|
104
|
+
{ userAgent: '*', allow: '/', disallow: ['/api/', '/dashboard/', '/admin/'] },
|
|
105
|
+
],
|
|
106
|
+
sitemap: 'https://yourdomain.com/sitemap.xml',
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Structured data (JSON-LD)
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
// app/blog/[slug]/page.tsx
|
|
115
|
+
export default function BlogPost({ post }) {
|
|
116
|
+
const jsonLd = {
|
|
117
|
+
'@context': 'https://schema.org',
|
|
118
|
+
'@type': 'Article',
|
|
119
|
+
headline: post.title,
|
|
120
|
+
datePublished: post.publishedAt,
|
|
121
|
+
dateModified: post.updatedAt,
|
|
122
|
+
author: { '@type': 'Person', name: post.author },
|
|
123
|
+
description: post.excerpt,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<>
|
|
128
|
+
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
|
129
|
+
{/* page content */}
|
|
130
|
+
</>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Common schemas: `Article`, `Product`, `Organization`, `BreadcrumbList`, `FAQPage`, `LocalBusiness`
|
|
136
|
+
|
|
137
|
+
## Technical SEO checklist
|
|
138
|
+
|
|
139
|
+
- [ ] `metadataBase` set in root layout (required for absolute OG URLs)
|
|
140
|
+
- [ ] `title.template` set for consistent page titles
|
|
141
|
+
- [ ] Description 50–160 chars on every page
|
|
142
|
+
- [ ] OG image 1200×630px on every page (static or generated)
|
|
143
|
+
- [ ] `sitemap.ts` includes all public pages
|
|
144
|
+
- [ ] `robots.ts` blocks `/api/`, `/dashboard/`, `/admin/`
|
|
145
|
+
- [ ] Canonical URL set on duplicate content pages
|
|
146
|
+
- [ ] Structured data on blog posts, products, FAQ
|
|
147
|
+
- [ ] No `noindex` accidentally on important pages
|
|
148
|
+
- [ ] Images have descriptive `alt` text
|
|
149
|
+
- [ ] Headings hierarchy: one `h1` per page, logical `h2`/`h3` structure
|
|
150
|
+
- [ ] Core Web Vitals passing (LCP <2.5s, CLS <0.1, INP <200ms)
|
|
151
|
+
|
|
152
|
+
## Quick wins (do these first)
|
|
153
|
+
|
|
154
|
+
1. Add `metadataBase` + default `title.template` (20 min)
|
|
155
|
+
2. Add OG image to every public page (1h)
|
|
156
|
+
3. Generate sitemap (30 min)
|
|
157
|
+
4. Add `robots.ts` (15 min)
|
|
158
|
+
5. Add JSON-LD to blog/product pages (1h)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-strategy
|
|
3
|
+
description: Testing strategy. Defines what to test, at what level, and how to mock. Prevents over-testing, under-testing, and tests that slow down development. Use before writing tests for a new feature or when a test suite grows painful.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Test Strategy
|
|
8
|
+
|
|
9
|
+
Test the behavior, not the implementation. A test that breaks when you rename a variable is worthless.
|
|
10
|
+
|
|
11
|
+
## The pyramid
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/‾‾‾‾‾‾\
|
|
15
|
+
/ E2E (5%) \ — Happy paths only. Slow, brittle, expensive.
|
|
16
|
+
/────────────\
|
|
17
|
+
/ Integration \ — API endpoints, DB queries, service boundaries.
|
|
18
|
+
/ (25–35%) \
|
|
19
|
+
/──────────────────\
|
|
20
|
+
/ Unit (60–70%) \ — Business logic, transformations, edge cases.
|
|
21
|
+
/──────────────────────\
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
If your pyramid is inverted (more E2E than unit), your tests are slow and fragile.
|
|
25
|
+
|
|
26
|
+
## What to test at each level
|
|
27
|
+
|
|
28
|
+
### Unit tests
|
|
29
|
+
Test pure functions, business logic, edge cases:
|
|
30
|
+
- Calculations, transformations, validations
|
|
31
|
+
- Error conditions and boundary values
|
|
32
|
+
- Utility functions
|
|
33
|
+
- State machines
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// GOOD — tests behavior
|
|
37
|
+
test('calculates discount correctly', () => {
|
|
38
|
+
expect(applyDiscount(100, 0.2)).toBe(80)
|
|
39
|
+
expect(applyDiscount(100, 0)).toBe(100)
|
|
40
|
+
expect(applyDiscount(0, 0.5)).toBe(0)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// BAD — tests implementation, breaks on refactor
|
|
44
|
+
test('calls Math.floor once', () => { ... })
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Integration tests
|
|
48
|
+
Test your API endpoints end-to-end (request → response, with real DB in test mode):
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
test('POST /api/v1/users creates user', async () => {
|
|
52
|
+
const res = await request(app)
|
|
53
|
+
.post('/api/v1/users')
|
|
54
|
+
.send({ email: 'test@example.com', name: 'Test' })
|
|
55
|
+
|
|
56
|
+
expect(res.status).toBe(201)
|
|
57
|
+
expect(res.body.data.email).toBe('test@example.com')
|
|
58
|
+
// verify it's actually in the DB
|
|
59
|
+
const user = await db.users.findByEmail('test@example.com')
|
|
60
|
+
expect(user).toBeTruthy()
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### E2E tests
|
|
65
|
+
Reserve for critical user paths only:
|
|
66
|
+
- Authentication (login, logout, forgot password)
|
|
67
|
+
- Checkout / payment flow
|
|
68
|
+
- Core value action of the product (publish, send, submit)
|
|
69
|
+
|
|
70
|
+
Never E2E test: form validation, UI states, error messages, edge cases.
|
|
71
|
+
|
|
72
|
+
## Mocking rules
|
|
73
|
+
|
|
74
|
+
| Mock this | Don't mock this |
|
|
75
|
+
|-----------|----------------|
|
|
76
|
+
| External APIs (Stripe, SendGrid, S3) | Internal business logic |
|
|
77
|
+
| Email/SMS sending | Database in unit tests (use in-memory or test DB) |
|
|
78
|
+
| Time (`Date.now()`, `new Date()`) | Your own service layer |
|
|
79
|
+
| Random values (`Math.random()`) | Framework code |
|
|
80
|
+
| File system in unit tests | |
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// GOOD — mock external, test logic
|
|
84
|
+
jest.mock('./email-service')
|
|
85
|
+
test('sends welcome email on registration', async () => {
|
|
86
|
+
await registerUser({ email: 'test@example.com' })
|
|
87
|
+
expect(sendWelcomeEmail).toHaveBeenCalledWith('test@example.com')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// BAD — mocking what you're testing
|
|
91
|
+
jest.mock('./user-service')
|
|
92
|
+
test('user service creates user', () => { ... }) // what are we even testing?
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Coverage as a signal, not a goal
|
|
96
|
+
|
|
97
|
+
- 80% coverage is fine. 100% is usually waste.
|
|
98
|
+
- Coverage doesn't measure quality — a test that does `expect(true).toBe(true)` counts.
|
|
99
|
+
- Focus on: critical paths, error branches, edge cases.
|
|
100
|
+
- Skip: trivial getters/setters, framework boilerplate, generated code.
|
|
101
|
+
|
|
102
|
+
## Red flags in test suites
|
|
103
|
+
|
|
104
|
+
- Tests that test implementation details (internals, private methods)
|
|
105
|
+
- `setTimeout` or `sleep` in tests (use fake timers or conditions)
|
|
106
|
+
- Tests that depend on execution order (each test must be independent)
|
|
107
|
+
- Snapshots for everything (become maintenance burden — use for UI components only)
|
|
108
|
+
- Mocking your own database service (test against a real test DB instead)
|
|
109
|
+
|
|
110
|
+
## Before writing tests
|
|
111
|
+
|
|
112
|
+
1. Identify the **behavior** you're testing (not the code)
|
|
113
|
+
2. Write the test name as a sentence: `"should return 404 when user doesn't exist"`
|
|
114
|
+
3. Arrange → Act → Assert — three clear sections, no more
|
|
115
|
+
4. One assertion per test (or one logical group)
|
|
116
|
+
5. Make the test fail first — if it passes without code, it's testing nothing
|
|
117
|
+
|
|
118
|
+
## Stack conventions
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
// Vitest (preferred for Vite/Next projects)
|
|
122
|
+
import { describe, test, expect, vi } from 'vitest'
|
|
123
|
+
|
|
124
|
+
// Jest (legacy, Node projects)
|
|
125
|
+
import { describe, test, expect, jest } from '@jest/globals'
|
|
126
|
+
|
|
127
|
+
// React Testing Library — test from user perspective
|
|
128
|
+
import { render, screen, userEvent } from '@testing-library/react'
|
|
129
|
+
test('shows error when email invalid', async () => {
|
|
130
|
+
render(<LoginForm />)
|
|
131
|
+
await userEvent.type(screen.getByLabelText('Email'), 'not-an-email')
|
|
132
|
+
await userEvent.click(screen.getByRole('button', { name: 'Login' }))
|
|
133
|
+
expect(screen.getByText('Invalid email')).toBeInTheDocument()
|
|
134
|
+
})
|
|
135
|
+
```
|