red64-cli 0.1.0 → 0.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/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +5 -0
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/components/init/CompleteStep.d.ts.map +1 -1
- package/dist/components/init/CompleteStep.js +2 -2
- package/dist/components/init/CompleteStep.js.map +1 -1
- package/dist/components/init/TestCheckStep.d.ts +16 -0
- package/dist/components/init/TestCheckStep.d.ts.map +1 -0
- package/dist/components/init/TestCheckStep.js +120 -0
- package/dist/components/init/TestCheckStep.js.map +1 -0
- package/dist/components/init/index.d.ts +1 -0
- package/dist/components/init/index.d.ts.map +1 -1
- package/dist/components/init/index.js +1 -0
- package/dist/components/init/index.js.map +1 -1
- package/dist/components/init/types.d.ts +9 -0
- package/dist/components/init/types.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.js +69 -6
- package/dist/components/screens/InitScreen.js.map +1 -1
- package/dist/components/screens/StartScreen.d.ts.map +1 -1
- package/dist/components/screens/StartScreen.js +89 -3
- package/dist/components/screens/StartScreen.js.map +1 -1
- package/dist/services/ConfigService.d.ts +1 -0
- package/dist/services/ConfigService.d.ts.map +1 -1
- package/dist/services/ConfigService.js.map +1 -1
- package/dist/services/ProjectDetector.d.ts +28 -0
- package/dist/services/ProjectDetector.d.ts.map +1 -0
- package/dist/services/ProjectDetector.js +236 -0
- package/dist/services/ProjectDetector.js.map +1 -0
- package/dist/services/TestRunner.d.ts +46 -0
- package/dist/services/TestRunner.d.ts.map +1 -0
- package/dist/services/TestRunner.js +85 -0
- package/dist/services/TestRunner.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
- package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
- package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
- package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
- package/framework/stacks/generic/feedback.md +80 -0
- package/framework/stacks/nextjs/accessibility.md +437 -0
- package/framework/stacks/nextjs/api.md +431 -0
- package/framework/stacks/nextjs/coding-style.md +282 -0
- package/framework/stacks/nextjs/commenting.md +226 -0
- package/framework/stacks/nextjs/components.md +411 -0
- package/framework/stacks/nextjs/conventions.md +333 -0
- package/framework/stacks/nextjs/css.md +310 -0
- package/framework/stacks/nextjs/error-handling.md +442 -0
- package/framework/stacks/nextjs/feedback.md +124 -0
- package/framework/stacks/nextjs/migrations.md +332 -0
- package/framework/stacks/nextjs/models.md +362 -0
- package/framework/stacks/nextjs/queries.md +410 -0
- package/framework/stacks/nextjs/responsive.md +338 -0
- package/framework/stacks/nextjs/tech-stack.md +177 -0
- package/framework/stacks/nextjs/test-writing.md +475 -0
- package/framework/stacks/nextjs/validation.md +467 -0
- package/framework/stacks/python/api.md +468 -0
- package/framework/stacks/python/authentication.md +342 -0
- package/framework/stacks/python/code-quality.md +283 -0
- package/framework/stacks/python/code-refactoring.md +315 -0
- package/framework/stacks/python/coding-style.md +462 -0
- package/framework/stacks/python/conventions.md +399 -0
- package/framework/stacks/python/error-handling.md +512 -0
- package/framework/stacks/python/feedback.md +92 -0
- package/framework/stacks/python/implement-ai-llm.md +468 -0
- package/framework/stacks/python/migrations.md +388 -0
- package/framework/stacks/python/models.md +399 -0
- package/framework/stacks/python/python.md +232 -0
- package/framework/stacks/python/queries.md +451 -0
- package/framework/stacks/python/structure.md +245 -58
- package/framework/stacks/python/tech.md +92 -35
- package/framework/stacks/python/testing.md +380 -0
- package/framework/stacks/python/validation.md +471 -0
- package/framework/stacks/rails/authentication.md +176 -0
- package/framework/stacks/rails/code-quality.md +287 -0
- package/framework/stacks/rails/code-refactoring.md +299 -0
- package/framework/stacks/rails/feedback.md +130 -0
- package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
- package/framework/stacks/rails/rails.md +301 -0
- package/framework/stacks/rails/rails8-best-practices.md +498 -0
- package/framework/stacks/rails/rails8-css.md +573 -0
- package/framework/stacks/rails/structure.md +140 -0
- package/framework/stacks/rails/tech.md +108 -0
- package/framework/stacks/react/code-quality.md +521 -0
- package/framework/stacks/react/components.md +625 -0
- package/framework/stacks/react/data-fetching.md +586 -0
- package/framework/stacks/react/feedback.md +110 -0
- package/framework/stacks/react/forms.md +694 -0
- package/framework/stacks/react/performance.md +640 -0
- package/framework/stacks/react/product.md +22 -9
- package/framework/stacks/react/state-management.md +472 -0
- package/framework/stacks/react/structure.md +351 -44
- package/framework/stacks/react/tech.md +219 -30
- package/framework/stacks/react/testing.md +690 -0
- package/package.json +1 -1
- package/framework/stacks/node/product.md +0 -27
- package/framework/stacks/node/structure.md +0 -82
- package/framework/stacks/node/tech.md +0 -63
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
# API Design Standards
|
|
2
|
+
|
|
3
|
+
Next.js App Router API conventions for route handlers, server actions, request validation, and consistent response patterns.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Server Actions first**: Use server actions for mutations; reserve route handlers for external/webhook APIs
|
|
10
|
+
- **Type-safe end-to-end**: Zod validates input, TypeScript types flow from server to client
|
|
11
|
+
- **Consistent responses**: Every route handler follows the same response envelope
|
|
12
|
+
- **Thin handlers**: Validation and response formatting in the handler, business logic in services
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Route Handler Conventions
|
|
17
|
+
|
|
18
|
+
### File Structure
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
app/
|
|
22
|
+
api/
|
|
23
|
+
health/
|
|
24
|
+
route.ts # GET /api/health
|
|
25
|
+
users/
|
|
26
|
+
route.ts # GET, POST /api/users
|
|
27
|
+
[id]/
|
|
28
|
+
route.ts # GET, PATCH, DELETE /api/users/:id
|
|
29
|
+
webhooks/
|
|
30
|
+
stripe/
|
|
31
|
+
route.ts # POST /api/webhooks/stripe
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Basic Route Handler
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// app/api/users/route.ts
|
|
38
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
39
|
+
import { z } from "zod";
|
|
40
|
+
import { prisma } from "@/lib/prisma";
|
|
41
|
+
import { auth } from "@/lib/auth";
|
|
42
|
+
|
|
43
|
+
const createUserSchema = z.object({
|
|
44
|
+
email: z.string().email(),
|
|
45
|
+
name: z.string().min(1).max(255),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export async function GET(request: NextRequest) {
|
|
49
|
+
const session = await auth();
|
|
50
|
+
if (!session) {
|
|
51
|
+
return NextResponse.json(
|
|
52
|
+
{ error: { code: "UNAUTHORIZED", message: "Authentication required" } },
|
|
53
|
+
{ status: 401 }
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { searchParams } = request.nextUrl;
|
|
58
|
+
const page = Number(searchParams.get("page") ?? "1");
|
|
59
|
+
const limit = Math.min(Number(searchParams.get("limit") ?? "20"), 100);
|
|
60
|
+
|
|
61
|
+
const [users, total] = await Promise.all([
|
|
62
|
+
prisma.user.findMany({
|
|
63
|
+
skip: (page - 1) * limit,
|
|
64
|
+
take: limit,
|
|
65
|
+
orderBy: { createdAt: "desc" },
|
|
66
|
+
select: { id: true, email: true, name: true, createdAt: true },
|
|
67
|
+
}),
|
|
68
|
+
prisma.user.count(),
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
return NextResponse.json({
|
|
72
|
+
items: users,
|
|
73
|
+
total,
|
|
74
|
+
page,
|
|
75
|
+
limit,
|
|
76
|
+
totalPages: Math.ceil(total / limit),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function POST(request: NextRequest) {
|
|
81
|
+
const session = await auth();
|
|
82
|
+
if (!session) {
|
|
83
|
+
return NextResponse.json(
|
|
84
|
+
{ error: { code: "UNAUTHORIZED", message: "Authentication required" } },
|
|
85
|
+
{ status: 401 }
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const body = await request.json();
|
|
90
|
+
const parsed = createUserSchema.safeParse(body);
|
|
91
|
+
|
|
92
|
+
if (!parsed.success) {
|
|
93
|
+
return NextResponse.json(
|
|
94
|
+
{ error: { code: "VALIDATION_ERROR", message: "Invalid input", details: parsed.error.flatten() } },
|
|
95
|
+
{ status: 422 }
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const user = await prisma.user.create({ data: parsed.data });
|
|
100
|
+
return NextResponse.json(user, { status: 201 });
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## HTTP Methods and Status Codes
|
|
107
|
+
|
|
108
|
+
### RESTful Conventions
|
|
109
|
+
|
|
110
|
+
| Method | URL Pattern | Action | Status |
|
|
111
|
+
|---|---|---|---|
|
|
112
|
+
| `GET` | `/api/users` | List users | 200 |
|
|
113
|
+
| `GET` | `/api/users/42` | Get single user | 200 |
|
|
114
|
+
| `POST` | `/api/users` | Create user | 201 |
|
|
115
|
+
| `PATCH` | `/api/users/42` | Partial update | 200 |
|
|
116
|
+
| `DELETE` | `/api/users/42` | Delete user | 204 |
|
|
117
|
+
|
|
118
|
+
### URL Naming Rules
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
# GOOD
|
|
122
|
+
GET /api/users
|
|
123
|
+
POST /api/users
|
|
124
|
+
GET /api/users/42/posts
|
|
125
|
+
|
|
126
|
+
# BAD
|
|
127
|
+
GET /api/getUsers
|
|
128
|
+
POST /api/createUser
|
|
129
|
+
GET /api/user/42/getAllPosts
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- Plural nouns for resources: `/users`, `/posts`
|
|
133
|
+
- Lowercase with hyphens for multi-word: `/api-keys`, `/user-profiles`
|
|
134
|
+
- No verbs in URLs (HTTP methods convey action)
|
|
135
|
+
- No trailing slashes
|
|
136
|
+
- Maximum two levels of nesting
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Response Envelope
|
|
141
|
+
|
|
142
|
+
### Success: Single Resource
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"id": 42,
|
|
147
|
+
"email": "user@example.com",
|
|
148
|
+
"name": "Jane Doe",
|
|
149
|
+
"createdAt": "2024-01-15T10:30:00.000Z"
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Success: Collection
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"items": [...],
|
|
158
|
+
"total": 142,
|
|
159
|
+
"page": 1,
|
|
160
|
+
"limit": 20,
|
|
161
|
+
"totalPages": 8
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Error Response
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"error": {
|
|
170
|
+
"code": "NOT_FOUND",
|
|
171
|
+
"message": "User not found",
|
|
172
|
+
"details": { "resource": "User", "id": "42" }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Standard Error Codes
|
|
178
|
+
|
|
179
|
+
| Code | HTTP Status | Meaning |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| `VALIDATION_ERROR` | 422 | Invalid request body or params |
|
|
182
|
+
| `UNAUTHORIZED` | 401 | Missing or invalid authentication |
|
|
183
|
+
| `FORBIDDEN` | 403 | Authenticated but insufficient permissions |
|
|
184
|
+
| `NOT_FOUND` | 404 | Resource does not exist |
|
|
185
|
+
| `CONFLICT` | 409 | Duplicate resource or state conflict |
|
|
186
|
+
| `RATE_LIMITED` | 429 | Too many requests |
|
|
187
|
+
| `INTERNAL_ERROR` | 500 | Unhandled server error |
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Server Actions vs Route Handlers
|
|
192
|
+
|
|
193
|
+
### When to Use Each
|
|
194
|
+
|
|
195
|
+
| Use Case | Approach |
|
|
196
|
+
|---|---|
|
|
197
|
+
| Form submissions | Server Action |
|
|
198
|
+
| Data mutations from UI | Server Action |
|
|
199
|
+
| External API consumers | Route Handler |
|
|
200
|
+
| Webhooks | Route Handler |
|
|
201
|
+
| File uploads with progress | Route Handler |
|
|
202
|
+
| CORS-enabled endpoints | Route Handler |
|
|
203
|
+
| Streaming responses | Route Handler |
|
|
204
|
+
|
|
205
|
+
### Server Action Pattern
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
// app/actions/users.ts
|
|
209
|
+
"use server";
|
|
210
|
+
|
|
211
|
+
import { z } from "zod";
|
|
212
|
+
import { auth } from "@/lib/auth";
|
|
213
|
+
import { prisma } from "@/lib/prisma";
|
|
214
|
+
import { revalidatePath } from "next/cache";
|
|
215
|
+
|
|
216
|
+
const updateProfileSchema = z.object({
|
|
217
|
+
name: z.string().min(1).max(255),
|
|
218
|
+
bio: z.string().max(500).optional(),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
export async function updateProfile(formData: FormData) {
|
|
222
|
+
const session = await auth();
|
|
223
|
+
if (!session?.user?.id) {
|
|
224
|
+
return { error: "Unauthorized" };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const parsed = updateProfileSchema.safeParse({
|
|
228
|
+
name: formData.get("name"),
|
|
229
|
+
bio: formData.get("bio"),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (!parsed.success) {
|
|
233
|
+
return { error: "Invalid input", fieldErrors: parsed.error.flatten().fieldErrors };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await prisma.user.update({
|
|
237
|
+
where: { id: session.user.id },
|
|
238
|
+
data: parsed.data,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
revalidatePath("/profile");
|
|
242
|
+
return { success: true };
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Middleware
|
|
249
|
+
|
|
250
|
+
### Authentication Middleware
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
// middleware.ts
|
|
254
|
+
import { auth } from "@/lib/auth";
|
|
255
|
+
import { NextResponse } from "next/server";
|
|
256
|
+
|
|
257
|
+
export default auth((req) => {
|
|
258
|
+
const isApiRoute = req.nextUrl.pathname.startsWith("/api");
|
|
259
|
+
const isPublicApi = req.nextUrl.pathname.startsWith("/api/health") ||
|
|
260
|
+
req.nextUrl.pathname.startsWith("/api/webhooks");
|
|
261
|
+
|
|
262
|
+
if (isApiRoute && !isPublicApi && !req.auth) {
|
|
263
|
+
return NextResponse.json(
|
|
264
|
+
{ error: { code: "UNAUTHORIZED", message: "Authentication required" } },
|
|
265
|
+
{ status: 401 }
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return NextResponse.next();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
export const config = {
|
|
273
|
+
matcher: ["/api/:path*", "/dashboard/:path*"],
|
|
274
|
+
};
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### CORS for Route Handlers
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// app/api/public/route.ts
|
|
281
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
282
|
+
|
|
283
|
+
const ALLOWED_ORIGINS = [
|
|
284
|
+
"https://example.com",
|
|
285
|
+
process.env.NODE_ENV === "development" && "http://localhost:3001",
|
|
286
|
+
].filter(Boolean) as string[];
|
|
287
|
+
|
|
288
|
+
function corsHeaders(origin: string | null) {
|
|
289
|
+
const headers = new Headers();
|
|
290
|
+
if (origin && ALLOWED_ORIGINS.includes(origin)) {
|
|
291
|
+
headers.set("Access-Control-Allow-Origin", origin);
|
|
292
|
+
headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
293
|
+
headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
294
|
+
headers.set("Access-Control-Max-Age", "86400");
|
|
295
|
+
}
|
|
296
|
+
return headers;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function OPTIONS(request: NextRequest) {
|
|
300
|
+
return new NextResponse(null, {
|
|
301
|
+
status: 204,
|
|
302
|
+
headers: corsHeaders(request.headers.get("origin")),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function GET(request: NextRequest) {
|
|
307
|
+
const data = { message: "Hello" };
|
|
308
|
+
return NextResponse.json(data, {
|
|
309
|
+
headers: corsHeaders(request.headers.get("origin")),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Rate Limiting
|
|
317
|
+
|
|
318
|
+
### Token Bucket with Headers
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
// lib/rate-limit.ts
|
|
322
|
+
const rateLimitMap = new Map<string, { tokens: number; lastRefill: number }>();
|
|
323
|
+
|
|
324
|
+
export function rateLimit(
|
|
325
|
+
key: string,
|
|
326
|
+
options: { limit: number; windowMs: number }
|
|
327
|
+
): { success: boolean; remaining: number; reset: number } {
|
|
328
|
+
const now = Date.now();
|
|
329
|
+
const record = rateLimitMap.get(key) ?? { tokens: options.limit, lastRefill: now };
|
|
330
|
+
|
|
331
|
+
const elapsed = now - record.lastRefill;
|
|
332
|
+
const refillRate = options.limit / options.windowMs;
|
|
333
|
+
record.tokens = Math.min(options.limit, record.tokens + elapsed * refillRate);
|
|
334
|
+
record.lastRefill = now;
|
|
335
|
+
|
|
336
|
+
if (record.tokens < 1) {
|
|
337
|
+
rateLimitMap.set(key, record);
|
|
338
|
+
return { success: false, remaining: 0, reset: Math.ceil(options.windowMs / 1000) };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
record.tokens -= 1;
|
|
342
|
+
rateLimitMap.set(key, record);
|
|
343
|
+
return { success: true, remaining: Math.floor(record.tokens), reset: Math.ceil(options.windowMs / 1000) };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Usage in route handler
|
|
347
|
+
export async function POST(request: NextRequest) {
|
|
348
|
+
const ip = request.headers.get("x-forwarded-for") ?? "unknown";
|
|
349
|
+
const { success, remaining, reset } = rateLimit(ip, { limit: 10, windowMs: 60_000 });
|
|
350
|
+
|
|
351
|
+
if (!success) {
|
|
352
|
+
return NextResponse.json(
|
|
353
|
+
{ error: { code: "RATE_LIMITED", message: "Too many requests" } },
|
|
354
|
+
{
|
|
355
|
+
status: 429,
|
|
356
|
+
headers: {
|
|
357
|
+
"X-RateLimit-Limit": "10",
|
|
358
|
+
"X-RateLimit-Remaining": String(remaining),
|
|
359
|
+
"X-RateLimit-Reset": String(reset),
|
|
360
|
+
"Retry-After": String(reset),
|
|
361
|
+
},
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ... handle request
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Streaming Responses
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
// app/api/stream/route.ts
|
|
376
|
+
export async function GET() {
|
|
377
|
+
const encoder = new TextEncoder();
|
|
378
|
+
|
|
379
|
+
const stream = new ReadableStream({
|
|
380
|
+
async start(controller) {
|
|
381
|
+
for (const chunk of ["Hello", " ", "World"]) {
|
|
382
|
+
controller.enqueue(encoder.encode(chunk));
|
|
383
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
384
|
+
}
|
|
385
|
+
controller.close();
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return new Response(stream, {
|
|
390
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Health Check
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
// app/api/health/route.ts
|
|
401
|
+
import { prisma } from "@/lib/prisma";
|
|
402
|
+
|
|
403
|
+
export async function GET() {
|
|
404
|
+
try {
|
|
405
|
+
await prisma.$queryRaw`SELECT 1`;
|
|
406
|
+
return Response.json({ status: "ok", database: "connected" });
|
|
407
|
+
} catch {
|
|
408
|
+
return Response.json(
|
|
409
|
+
{ status: "error", database: "disconnected" },
|
|
410
|
+
{ status: 503 }
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## Anti-Patterns
|
|
419
|
+
|
|
420
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
421
|
+
|---|---|---|
|
|
422
|
+
| Verbs in URLs | Not RESTful | Use HTTP methods to convey action |
|
|
423
|
+
| Using route handlers for form submissions | Unnecessary complexity | Use server actions for UI mutations |
|
|
424
|
+
| No input validation | Security risk, bad data | Validate with zod in every handler |
|
|
425
|
+
| Business logic in route handlers | Hard to test, duplicated | Extract to service functions |
|
|
426
|
+
| Returning raw Prisma errors | Leaks schema details | Map to error envelope with safe messages |
|
|
427
|
+
| No rate limiting on auth endpoints | Brute force vulnerability | Rate limit login and signup endpoints |
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
_APIs are contracts. Validate every input, standardize every output, and keep business logic out of handlers._
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Coding Style
|
|
2
|
+
|
|
3
|
+
TypeScript and React conventions for Next.js 15 App Router projects with strict type safety and consistent patterns.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Strict TypeScript**: Enable all strict checks; `any` is a code smell, not a solution
|
|
10
|
+
- **Explicit over implicit**: Named exports, explicit return types on public functions, no magic
|
|
11
|
+
- **Consistency**: One way to do things, enforced by tooling, not willpower
|
|
12
|
+
- **Functional React**: Function components, hooks, composition over inheritance
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## TypeScript Configuration
|
|
17
|
+
|
|
18
|
+
### Strict Mode (Non-Negotiable)
|
|
19
|
+
|
|
20
|
+
```jsonc
|
|
21
|
+
// tsconfig.json
|
|
22
|
+
{
|
|
23
|
+
"compilerOptions": {
|
|
24
|
+
"strict": true,
|
|
25
|
+
"noUncheckedIndexedAccess": true,
|
|
26
|
+
"noImplicitReturns": true,
|
|
27
|
+
"noFallthroughCasesInSwitch": true,
|
|
28
|
+
"exactOptionalPropertyTypes": true,
|
|
29
|
+
"forceConsistentCasingInFileNames": true,
|
|
30
|
+
"moduleResolution": "bundler",
|
|
31
|
+
"paths": {
|
|
32
|
+
"@/*": ["./src/*"]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Type vs Interface
|
|
39
|
+
|
|
40
|
+
| Use | When |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `type` | Unions, intersections, mapped types, utility types |
|
|
43
|
+
| `interface` | Object shapes that may be extended, component props |
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// Interface for props (extendable)
|
|
47
|
+
interface ButtonProps {
|
|
48
|
+
variant: "primary" | "secondary";
|
|
49
|
+
size?: "sm" | "md" | "lg";
|
|
50
|
+
children: React.ReactNode;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Type for unions and utility types
|
|
54
|
+
type ApiResponse<T> = { data: T; error: null } | { data: null; error: string };
|
|
55
|
+
type UserRole = "admin" | "member" | "viewer";
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Rule**: Be consistent within a file. When in doubt, use `interface` for props and `type` for everything else.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Naming Conventions
|
|
63
|
+
|
|
64
|
+
| Entity | Convention | Example |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| Components | PascalCase | `UserProfile`, `DataTable` |
|
|
67
|
+
| Component files | PascalCase or kebab-case | `UserProfile.tsx` or `user-profile.tsx` |
|
|
68
|
+
| Hooks | camelCase with `use` prefix | `useAuth`, `useDebounce` |
|
|
69
|
+
| Utilities | camelCase | `formatDate`, `cn` |
|
|
70
|
+
| Constants | UPPER_SNAKE_CASE | `MAX_RETRIES`, `API_BASE_URL` |
|
|
71
|
+
| Types/Interfaces | PascalCase | `UserProfile`, `ApiResponse` |
|
|
72
|
+
| Enums | PascalCase (members too) | `UserRole.Admin` |
|
|
73
|
+
| Route files | lowercase (Next.js convention) | `page.tsx`, `layout.tsx`, `route.ts` |
|
|
74
|
+
| Server actions | camelCase verb phrases | `createUser`, `updateProfile` |
|
|
75
|
+
| Zod schemas | camelCase + Schema suffix | `createUserSchema`, `loginSchema` |
|
|
76
|
+
| Environment variables | UPPER_SNAKE_CASE | `DATABASE_URL`, `NEXT_PUBLIC_APP_NAME` |
|
|
77
|
+
|
|
78
|
+
### Boolean Naming
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// Prefix with is, has, can, should
|
|
82
|
+
const isLoading = true;
|
|
83
|
+
const hasPermission = user.role === "admin";
|
|
84
|
+
const canEdit = hasPermission && !isArchived;
|
|
85
|
+
const shouldRefetch = Date.now() - lastFetch > STALE_TIME;
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Function Components
|
|
91
|
+
|
|
92
|
+
### Component Structure
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// 1. Imports
|
|
96
|
+
import { type ComponentProps } from "react";
|
|
97
|
+
import { cn } from "@/lib/utils";
|
|
98
|
+
|
|
99
|
+
// 2. Types
|
|
100
|
+
interface CardProps {
|
|
101
|
+
title: string;
|
|
102
|
+
description?: string;
|
|
103
|
+
children: React.ReactNode;
|
|
104
|
+
className?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 3. Component (named export)
|
|
108
|
+
export function Card({ title, description, children, className }: CardProps) {
|
|
109
|
+
return (
|
|
110
|
+
<div className={cn("rounded-lg border p-6", className)}>
|
|
111
|
+
<h3 className="text-lg font-semibold">{title}</h3>
|
|
112
|
+
{description && <p className="text-muted-foreground mt-1">{description}</p>}
|
|
113
|
+
<div className="mt-4">{children}</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Rules
|
|
120
|
+
|
|
121
|
+
- Named exports only (no `export default` except for `page.tsx`, `layout.tsx`, and other Next.js conventions)
|
|
122
|
+
- Props interface defined above the component
|
|
123
|
+
- Destructure props in the function signature
|
|
124
|
+
- No `React.FC` -- use plain function with typed props
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Import Organization
|
|
129
|
+
|
|
130
|
+
### Order (Enforced by ESLint)
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// 1. React / Next.js
|
|
134
|
+
import { useState, useCallback } from "react";
|
|
135
|
+
import { useRouter } from "next/navigation";
|
|
136
|
+
import Image from "next/image";
|
|
137
|
+
|
|
138
|
+
// 2. External libraries
|
|
139
|
+
import { z } from "zod";
|
|
140
|
+
import { useForm } from "react-hook-form";
|
|
141
|
+
|
|
142
|
+
// 3. Internal aliases (@/)
|
|
143
|
+
import { prisma } from "@/lib/prisma";
|
|
144
|
+
import { cn } from "@/lib/utils";
|
|
145
|
+
import { Button } from "@/components/ui/button";
|
|
146
|
+
|
|
147
|
+
// 4. Relative imports
|
|
148
|
+
import { UserAvatar } from "./user-avatar";
|
|
149
|
+
|
|
150
|
+
// 5. Types (type-only imports)
|
|
151
|
+
import type { User } from "@prisma/client";
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Barrel Exports
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// components/ui/index.ts -- use sparingly
|
|
158
|
+
export { Button } from "./button";
|
|
159
|
+
export { Input } from "./input";
|
|
160
|
+
export { Card } from "./card";
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Warning**: Barrel exports can hurt tree-shaking and dev server performance. Use them for UI component libraries only, not for feature modules.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Hooks Patterns
|
|
168
|
+
|
|
169
|
+
### Custom Hook Structure
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// hooks/use-debounce.ts
|
|
173
|
+
import { useState, useEffect } from "react";
|
|
174
|
+
|
|
175
|
+
export function useDebounce<T>(value: T, delay: number): T {
|
|
176
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
177
|
+
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
const timer = setTimeout(() => setDebouncedValue(value), delay);
|
|
180
|
+
return () => clearTimeout(timer);
|
|
181
|
+
}, [value, delay]);
|
|
182
|
+
|
|
183
|
+
return debouncedValue;
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Hook Rules
|
|
188
|
+
|
|
189
|
+
- One hook per file, named after the hook
|
|
190
|
+
- Always return a stable API (avoid returning new object references)
|
|
191
|
+
- Prefer returning tuples for simple state: `[value, setValue]`
|
|
192
|
+
- Prefer returning objects for complex state: `{ data, error, isLoading }`
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## ESLint and Prettier
|
|
197
|
+
|
|
198
|
+
### ESLint Flat Config
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
// eslint.config.mjs
|
|
202
|
+
import { FlatCompat } from "@eslint/eslintrc";
|
|
203
|
+
|
|
204
|
+
const compat = new FlatCompat({ baseDirectory: import.meta.dirname });
|
|
205
|
+
|
|
206
|
+
export default [
|
|
207
|
+
...compat.extends("next/core-web-vitals", "next/typescript"),
|
|
208
|
+
{
|
|
209
|
+
rules: {
|
|
210
|
+
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
|
211
|
+
"@typescript-eslint/no-explicit-any": "error",
|
|
212
|
+
"prefer-const": "error",
|
|
213
|
+
"no-console": ["warn", { allow: ["warn", "error"] }],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Prettier Config
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"semi": true,
|
|
224
|
+
"singleQuote": false,
|
|
225
|
+
"tabWidth": 2,
|
|
226
|
+
"trailingComma": "es5",
|
|
227
|
+
"printWidth": 100,
|
|
228
|
+
"plugins": ["prettier-plugin-tailwindcss"]
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Module Organization
|
|
235
|
+
|
|
236
|
+
### Feature Modules
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
src/
|
|
240
|
+
app/ # Next.js routes
|
|
241
|
+
components/
|
|
242
|
+
ui/ # Reusable UI primitives (Button, Input, Card)
|
|
243
|
+
forms/ # Form-specific components
|
|
244
|
+
layouts/ # Layout components (Sidebar, Header)
|
|
245
|
+
lib/
|
|
246
|
+
prisma.ts # Prisma client singleton
|
|
247
|
+
auth.ts # NextAuth configuration
|
|
248
|
+
utils.ts # General utilities (cn, formatDate)
|
|
249
|
+
hooks/ # Custom React hooks
|
|
250
|
+
actions/ # Server actions
|
|
251
|
+
types/ # Shared type definitions
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Avoid Deep Nesting
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// GOOD: Flat imports with aliases
|
|
258
|
+
import { Button } from "@/components/ui/button";
|
|
259
|
+
import { prisma } from "@/lib/prisma";
|
|
260
|
+
|
|
261
|
+
// BAD: Deep relative paths
|
|
262
|
+
import { Button } from "../../../components/ui/button";
|
|
263
|
+
import { prisma } from "../../../lib/prisma";
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Anti-Patterns
|
|
269
|
+
|
|
270
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
271
|
+
|---|---|---|
|
|
272
|
+
| `any` type | Bypasses type checking entirely | Use `unknown` and narrow, or define proper types |
|
|
273
|
+
| `export default` everywhere | Inconsistent naming on import | Named exports except Next.js conventions |
|
|
274
|
+
| `React.FC` | Legacy, issues with generics | Plain function with typed props |
|
|
275
|
+
| Barrel exports for features | Breaks tree-shaking, slow HMR | Direct imports for feature modules |
|
|
276
|
+
| `as` type assertions | Unsafe, hides errors | Type guards and narrowing |
|
|
277
|
+
| Inline types in function signatures | Unreadable, not reusable | Define named types and interfaces |
|
|
278
|
+
| `// @ts-ignore` | Silences real errors | Fix the type, or use `// @ts-expect-error` with explanation |
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
_TypeScript exists to catch bugs before they ship. Configure it strictly, trust the compiler, and never silence it without a comment explaining why._
|