autoworkflow 3.1.5 → 3.5.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/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
# REST API Skill
|
|
2
|
+
|
|
3
|
+
## RESTful Route Design
|
|
4
|
+
\`\`\`
|
|
5
|
+
# Resources use nouns (plural)
|
|
6
|
+
GET /api/users # List users
|
|
7
|
+
GET /api/users/:id # Get single user
|
|
8
|
+
POST /api/users # Create user
|
|
9
|
+
PUT /api/users/:id # Replace user (full update)
|
|
10
|
+
PATCH /api/users/:id # Partial update
|
|
11
|
+
DELETE /api/users/:id # Delete user
|
|
12
|
+
|
|
13
|
+
# Nested resources
|
|
14
|
+
GET /api/users/:id/posts # User's posts
|
|
15
|
+
POST /api/users/:id/posts # Create post for user
|
|
16
|
+
GET /api/posts/:id/comments # Post's comments
|
|
17
|
+
|
|
18
|
+
# Actions (when CRUD doesn't fit)
|
|
19
|
+
POST /api/users/:id/verify # Verify user
|
|
20
|
+
POST /api/posts/:id/publish # Publish post
|
|
21
|
+
POST /api/auth/login # Login
|
|
22
|
+
POST /api/auth/logout # Logout
|
|
23
|
+
|
|
24
|
+
# Filtering, sorting, pagination
|
|
25
|
+
GET /api/users?status=active&role=admin
|
|
26
|
+
GET /api/users?sort=created_at&order=desc
|
|
27
|
+
GET /api/users?page=2&limit=20
|
|
28
|
+
GET /api/users?cursor=abc123&limit=20
|
|
29
|
+
GET /api/posts?search=typescript&tags=react,node
|
|
30
|
+
\`\`\`
|
|
31
|
+
|
|
32
|
+
## Express.js Implementation
|
|
33
|
+
\`\`\`typescript
|
|
34
|
+
// routes/users.ts
|
|
35
|
+
import { Router } from 'express';
|
|
36
|
+
import { z } from 'zod';
|
|
37
|
+
import { prisma } from '../lib/prisma';
|
|
38
|
+
import { authenticate } from '../middleware/auth';
|
|
39
|
+
import { validate } from '../middleware/validate';
|
|
40
|
+
import { ApiError } from '../lib/errors';
|
|
41
|
+
|
|
42
|
+
const router = Router();
|
|
43
|
+
|
|
44
|
+
// Validation schemas
|
|
45
|
+
const createUserSchema = z.object({
|
|
46
|
+
body: z.object({
|
|
47
|
+
email: z.string().email(),
|
|
48
|
+
name: z.string().min(2).max(100),
|
|
49
|
+
password: z.string().min(8),
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const updateUserSchema = z.object({
|
|
54
|
+
body: z.object({
|
|
55
|
+
name: z.string().min(2).max(100).optional(),
|
|
56
|
+
bio: z.string().max(500).optional(),
|
|
57
|
+
}),
|
|
58
|
+
params: z.object({
|
|
59
|
+
id: z.string().uuid(),
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const listUsersSchema = z.object({
|
|
64
|
+
query: z.object({
|
|
65
|
+
page: z.coerce.number().min(1).default(1),
|
|
66
|
+
limit: z.coerce.number().min(1).max(100).default(20),
|
|
67
|
+
status: z.enum(['active', 'inactive']).optional(),
|
|
68
|
+
search: z.string().optional(),
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// List users with pagination
|
|
73
|
+
router.get('/', validate(listUsersSchema), async (req, res) => {
|
|
74
|
+
const { page, limit, status, search } = req.query;
|
|
75
|
+
|
|
76
|
+
const where = {
|
|
77
|
+
...(status && { status }),
|
|
78
|
+
...(search && {
|
|
79
|
+
OR: [
|
|
80
|
+
{ name: { contains: search, mode: 'insensitive' } },
|
|
81
|
+
{ email: { contains: search, mode: 'insensitive' } },
|
|
82
|
+
],
|
|
83
|
+
}),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const [users, total] = await Promise.all([
|
|
87
|
+
prisma.user.findMany({
|
|
88
|
+
where,
|
|
89
|
+
skip: (page - 1) * limit,
|
|
90
|
+
take: limit,
|
|
91
|
+
orderBy: { createdAt: 'desc' },
|
|
92
|
+
select: {
|
|
93
|
+
id: true,
|
|
94
|
+
name: true,
|
|
95
|
+
email: true,
|
|
96
|
+
createdAt: true,
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
prisma.user.count({ where }),
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
res.json({
|
|
103
|
+
data: users,
|
|
104
|
+
meta: {
|
|
105
|
+
page,
|
|
106
|
+
limit,
|
|
107
|
+
total,
|
|
108
|
+
totalPages: Math.ceil(total / limit),
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Get single user
|
|
114
|
+
router.get('/:id', async (req, res) => {
|
|
115
|
+
const user = await prisma.user.findUnique({
|
|
116
|
+
where: { id: req.params.id },
|
|
117
|
+
select: {
|
|
118
|
+
id: true,
|
|
119
|
+
name: true,
|
|
120
|
+
email: true,
|
|
121
|
+
bio: true,
|
|
122
|
+
createdAt: true,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (!user) {
|
|
127
|
+
throw new ApiError(404, 'USER_NOT_FOUND', 'User not found');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
res.json({ data: user });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Create user
|
|
134
|
+
router.post('/', validate(createUserSchema), async (req, res) => {
|
|
135
|
+
const { email, name, password } = req.body;
|
|
136
|
+
|
|
137
|
+
const existing = await prisma.user.findUnique({ where: { email } });
|
|
138
|
+
if (existing) {
|
|
139
|
+
throw new ApiError(409, 'EMAIL_EXISTS', 'Email already registered');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const hashedPassword = await bcrypt.hash(password, 12);
|
|
143
|
+
const user = await prisma.user.create({
|
|
144
|
+
data: { email, name, password: hashedPassword },
|
|
145
|
+
select: { id: true, email: true, name: true },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
res.status(201).json({ data: user });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Update user (partial)
|
|
152
|
+
router.patch('/:id', authenticate, validate(updateUserSchema), async (req, res) => {
|
|
153
|
+
if (req.user.id !== req.params.id && req.user.role !== 'admin') {
|
|
154
|
+
throw new ApiError(403, 'FORBIDDEN', 'Not authorized');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const user = await prisma.user.update({
|
|
158
|
+
where: { id: req.params.id },
|
|
159
|
+
data: req.body,
|
|
160
|
+
select: { id: true, name: true, bio: true },
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
res.json({ data: user });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Delete user
|
|
167
|
+
router.delete('/:id', authenticate, async (req, res) => {
|
|
168
|
+
if (req.user.id !== req.params.id && req.user.role !== 'admin') {
|
|
169
|
+
throw new ApiError(403, 'FORBIDDEN', 'Not authorized');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await prisma.user.delete({ where: { id: req.params.id } });
|
|
173
|
+
res.status(204).send();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
export default router;
|
|
177
|
+
\`\`\`
|
|
178
|
+
|
|
179
|
+
## Next.js App Router API
|
|
180
|
+
\`\`\`typescript
|
|
181
|
+
// app/api/users/route.ts
|
|
182
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
183
|
+
import { z } from 'zod';
|
|
184
|
+
import { prisma } from '@/lib/prisma';
|
|
185
|
+
import { getSession } from '@/lib/auth';
|
|
186
|
+
|
|
187
|
+
const createUserSchema = z.object({
|
|
188
|
+
email: z.string().email(),
|
|
189
|
+
name: z.string().min(2),
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// GET /api/users
|
|
193
|
+
export async function GET(request: NextRequest) {
|
|
194
|
+
const { searchParams } = new URL(request.url);
|
|
195
|
+
const page = Number(searchParams.get('page')) || 1;
|
|
196
|
+
const limit = Math.min(Number(searchParams.get('limit')) || 20, 100);
|
|
197
|
+
|
|
198
|
+
const [users, total] = await Promise.all([
|
|
199
|
+
prisma.user.findMany({
|
|
200
|
+
skip: (page - 1) * limit,
|
|
201
|
+
take: limit,
|
|
202
|
+
}),
|
|
203
|
+
prisma.user.count(),
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
return NextResponse.json({
|
|
207
|
+
data: users,
|
|
208
|
+
meta: { page, limit, total },
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// POST /api/users
|
|
213
|
+
export async function POST(request: NextRequest) {
|
|
214
|
+
try {
|
|
215
|
+
const body = await request.json();
|
|
216
|
+
const validated = createUserSchema.parse(body);
|
|
217
|
+
|
|
218
|
+
const user = await prisma.user.create({
|
|
219
|
+
data: validated,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return NextResponse.json({ data: user }, { status: 201 });
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (error instanceof z.ZodError) {
|
|
225
|
+
return NextResponse.json(
|
|
226
|
+
{
|
|
227
|
+
error: {
|
|
228
|
+
code: 'VALIDATION_ERROR',
|
|
229
|
+
message: 'Invalid input',
|
|
230
|
+
details: error.errors,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{ status: 400 }
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// app/api/users/[id]/route.ts
|
|
241
|
+
export async function GET(
|
|
242
|
+
request: NextRequest,
|
|
243
|
+
{ params }: { params: { id: string } }
|
|
244
|
+
) {
|
|
245
|
+
const user = await prisma.user.findUnique({
|
|
246
|
+
where: { id: params.id },
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (!user) {
|
|
250
|
+
return NextResponse.json(
|
|
251
|
+
{ error: { code: 'NOT_FOUND', message: 'User not found' } },
|
|
252
|
+
{ status: 404 }
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return NextResponse.json({ data: user });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function PATCH(
|
|
260
|
+
request: NextRequest,
|
|
261
|
+
{ params }: { params: { id: string } }
|
|
262
|
+
) {
|
|
263
|
+
const session = await getSession();
|
|
264
|
+
if (!session) {
|
|
265
|
+
return NextResponse.json(
|
|
266
|
+
{ error: { code: 'UNAUTHORIZED', message: 'Not authenticated' } },
|
|
267
|
+
{ status: 401 }
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const body = await request.json();
|
|
272
|
+
const user = await prisma.user.update({
|
|
273
|
+
where: { id: params.id },
|
|
274
|
+
data: body,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
return NextResponse.json({ data: user });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function DELETE(
|
|
281
|
+
request: NextRequest,
|
|
282
|
+
{ params }: { params: { id: string } }
|
|
283
|
+
) {
|
|
284
|
+
await prisma.user.delete({ where: { id: params.id } });
|
|
285
|
+
return new NextResponse(null, { status: 204 });
|
|
286
|
+
}
|
|
287
|
+
\`\`\`
|
|
288
|
+
|
|
289
|
+
## Middleware
|
|
290
|
+
\`\`\`typescript
|
|
291
|
+
// middleware/auth.ts
|
|
292
|
+
import { Request, Response, NextFunction } from 'express';
|
|
293
|
+
import jwt from 'jsonwebtoken';
|
|
294
|
+
import { ApiError } from '../lib/errors';
|
|
295
|
+
|
|
296
|
+
export async function authenticate(
|
|
297
|
+
req: Request,
|
|
298
|
+
res: Response,
|
|
299
|
+
next: NextFunction
|
|
300
|
+
) {
|
|
301
|
+
const authHeader = req.headers.authorization;
|
|
302
|
+
|
|
303
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
304
|
+
throw new ApiError(401, 'UNAUTHORIZED', 'Missing authentication token');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const token = authHeader.split(' ')[1];
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
|
|
311
|
+
req.user = decoded as User;
|
|
312
|
+
next();
|
|
313
|
+
} catch {
|
|
314
|
+
throw new ApiError(401, 'INVALID_TOKEN', 'Invalid or expired token');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// middleware/validate.ts
|
|
319
|
+
import { Request, Response, NextFunction } from 'express';
|
|
320
|
+
import { AnyZodObject, ZodError } from 'zod';
|
|
321
|
+
|
|
322
|
+
export function validate(schema: AnyZodObject) {
|
|
323
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
324
|
+
try {
|
|
325
|
+
await schema.parseAsync({
|
|
326
|
+
body: req.body,
|
|
327
|
+
query: req.query,
|
|
328
|
+
params: req.params,
|
|
329
|
+
});
|
|
330
|
+
next();
|
|
331
|
+
} catch (error) {
|
|
332
|
+
if (error instanceof ZodError) {
|
|
333
|
+
res.status(400).json({
|
|
334
|
+
error: {
|
|
335
|
+
code: 'VALIDATION_ERROR',
|
|
336
|
+
message: 'Invalid request data',
|
|
337
|
+
details: error.errors.map((e) => ({
|
|
338
|
+
field: e.path.join('.'),
|
|
339
|
+
message: e.message,
|
|
340
|
+
})),
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
} else {
|
|
344
|
+
next(error);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// middleware/rateLimit.ts
|
|
351
|
+
import rateLimit from 'express-rate-limit';
|
|
352
|
+
|
|
353
|
+
export const apiLimiter = rateLimit({
|
|
354
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
355
|
+
max: 100,
|
|
356
|
+
message: {
|
|
357
|
+
error: {
|
|
358
|
+
code: 'TOO_MANY_REQUESTS',
|
|
359
|
+
message: 'Too many requests, please try again later',
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
\`\`\`
|
|
364
|
+
|
|
365
|
+
## Error Handling
|
|
366
|
+
\`\`\`typescript
|
|
367
|
+
// lib/errors.ts
|
|
368
|
+
export class ApiError extends Error {
|
|
369
|
+
constructor(
|
|
370
|
+
public statusCode: number,
|
|
371
|
+
public code: string,
|
|
372
|
+
message: string,
|
|
373
|
+
public details?: unknown
|
|
374
|
+
) {
|
|
375
|
+
super(message);
|
|
376
|
+
this.name = 'ApiError';
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// middleware/errorHandler.ts
|
|
381
|
+
import { Request, Response, NextFunction } from 'express';
|
|
382
|
+
import { ApiError } from '../lib/errors';
|
|
383
|
+
|
|
384
|
+
export function errorHandler(
|
|
385
|
+
error: Error,
|
|
386
|
+
req: Request,
|
|
387
|
+
res: Response,
|
|
388
|
+
next: NextFunction
|
|
389
|
+
) {
|
|
390
|
+
console.error(error);
|
|
391
|
+
|
|
392
|
+
if (error instanceof ApiError) {
|
|
393
|
+
return res.status(error.statusCode).json({
|
|
394
|
+
error: {
|
|
395
|
+
code: error.code,
|
|
396
|
+
message: error.message,
|
|
397
|
+
...(error.details && { details: error.details }),
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Default to 500
|
|
403
|
+
res.status(500).json({
|
|
404
|
+
error: {
|
|
405
|
+
code: 'INTERNAL_ERROR',
|
|
406
|
+
message: 'An unexpected error occurred',
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
\`\`\`
|
|
411
|
+
|
|
412
|
+
## HTTP Status Codes
|
|
413
|
+
\`\`\`
|
|
414
|
+
# Success
|
|
415
|
+
200 OK - GET, PUT, PATCH success
|
|
416
|
+
201 Created - POST success (include Location header)
|
|
417
|
+
204 No Content - DELETE success
|
|
418
|
+
|
|
419
|
+
# Client Errors
|
|
420
|
+
400 Bad Request - Validation error, malformed request
|
|
421
|
+
401 Unauthorized - Missing/invalid authentication
|
|
422
|
+
403 Forbidden - Authenticated but not authorized
|
|
423
|
+
404 Not Found - Resource doesn't exist
|
|
424
|
+
409 Conflict - Duplicate resource, state conflict
|
|
425
|
+
422 Unprocessable - Valid JSON but semantic errors
|
|
426
|
+
429 Too Many Reqs - Rate limited
|
|
427
|
+
|
|
428
|
+
# Server Errors
|
|
429
|
+
500 Internal Error - Unexpected server error
|
|
430
|
+
502 Bad Gateway - Upstream service error
|
|
431
|
+
503 Unavailable - Temporarily unavailable
|
|
432
|
+
\`\`\`
|
|
433
|
+
|
|
434
|
+
## Response Formats
|
|
435
|
+
\`\`\`json
|
|
436
|
+
// Success with data
|
|
437
|
+
{
|
|
438
|
+
"data": { "id": "1", "name": "John" }
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Success with list and pagination
|
|
442
|
+
{
|
|
443
|
+
"data": [{ "id": "1" }, { "id": "2" }],
|
|
444
|
+
"meta": {
|
|
445
|
+
"page": 1,
|
|
446
|
+
"limit": 20,
|
|
447
|
+
"total": 100,
|
|
448
|
+
"totalPages": 5
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Cursor pagination
|
|
453
|
+
{
|
|
454
|
+
"data": [...],
|
|
455
|
+
"meta": {
|
|
456
|
+
"nextCursor": "abc123",
|
|
457
|
+
"hasMore": true
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Error
|
|
462
|
+
{
|
|
463
|
+
"error": {
|
|
464
|
+
"code": "VALIDATION_ERROR",
|
|
465
|
+
"message": "Invalid input",
|
|
466
|
+
"details": [
|
|
467
|
+
{ "field": "email", "message": "Invalid email format" }
|
|
468
|
+
]
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
\`\`\`
|
|
472
|
+
|
|
473
|
+
## ❌ DON'T
|
|
474
|
+
- Use verbs in URLs (/api/getUsers)
|
|
475
|
+
- Return 200 for errors
|
|
476
|
+
- Expose internal error details
|
|
477
|
+
- Skip input validation
|
|
478
|
+
- Ignore authentication/authorization
|
|
479
|
+
- Use inconsistent response formats
|
|
480
|
+
|
|
481
|
+
## ✅ DO
|
|
482
|
+
- Use plural nouns for resources
|
|
483
|
+
- Return appropriate status codes
|
|
484
|
+
- Validate all inputs with schema
|
|
485
|
+
- Use consistent error format
|
|
486
|
+
- Implement pagination for lists
|
|
487
|
+
- Add rate limiting
|
|
488
|
+
- Include CORS headers
|
|
489
|
+
- Version your API (/api/v1/...)
|
|
490
|
+
- Document with OpenAPI/Swagger
|