@girardmedia/bootspring 1.1.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/LICENSE +21 -0
- package/README.md +255 -0
- package/agents/README.md +93 -0
- package/agents/api-expert/context.md +416 -0
- package/agents/architecture-expert/context.md +454 -0
- package/agents/backend-expert/context.md +483 -0
- package/agents/code-review-expert/context.md +365 -0
- package/agents/database-expert/context.md +250 -0
- package/agents/devops-expert/context.md +446 -0
- package/agents/frontend-expert/context.md +364 -0
- package/agents/index.js +140 -0
- package/agents/performance-expert/context.md +377 -0
- package/agents/security-expert/context.md +343 -0
- package/agents/testing-expert/context.md +414 -0
- package/agents/ui-ux-expert/context.md +448 -0
- package/agents/vercel-expert/context.md +426 -0
- package/bin/bootspring.js +310 -0
- package/cli/agent.js +337 -0
- package/cli/context.js +194 -0
- package/cli/dashboard.js +150 -0
- package/cli/generate.js +294 -0
- package/cli/init.js +410 -0
- package/cli/loop.js +421 -0
- package/cli/mcp.js +241 -0
- package/cli/memory.js +303 -0
- package/cli/orchestrator.js +400 -0
- package/cli/plugin.js +451 -0
- package/cli/quality.js +332 -0
- package/cli/skill.js +369 -0
- package/cli/task.js +628 -0
- package/cli/telemetry.js +114 -0
- package/cli/todo.js +614 -0
- package/cli/update.js +312 -0
- package/core/config.js +245 -0
- package/core/context.js +329 -0
- package/core/entitlements.js +209 -0
- package/core/index.js +43 -0
- package/core/policies.js +68 -0
- package/core/telemetry.js +247 -0
- package/core/utils.js +380 -0
- package/dashboard/server.js +818 -0
- package/docs/integrations/claude-code.md +42 -0
- package/docs/integrations/codex.md +42 -0
- package/docs/mcp-api-platform.md +102 -0
- package/generators/generate.js +598 -0
- package/generators/index.js +18 -0
- package/hooks/context-detector.js +177 -0
- package/hooks/index.js +35 -0
- package/hooks/prompt-enhancer.js +289 -0
- package/intelligence/git-memory.js +551 -0
- package/intelligence/index.js +59 -0
- package/intelligence/orchestrator.js +964 -0
- package/intelligence/prd.js +447 -0
- package/intelligence/recommendation-weights.json +18 -0
- package/intelligence/recommendations.js +234 -0
- package/mcp/capabilities.js +71 -0
- package/mcp/contracts/mcp-contract.v1.json +497 -0
- package/mcp/registry.js +213 -0
- package/mcp/response-formatter.js +462 -0
- package/mcp/server.js +99 -0
- package/mcp/tools/agent-tool.js +137 -0
- package/mcp/tools/capabilities-tool.js +54 -0
- package/mcp/tools/context-tool.js +49 -0
- package/mcp/tools/dashboard-tool.js +58 -0
- package/mcp/tools/generate-tool.js +46 -0
- package/mcp/tools/loop-tool.js +134 -0
- package/mcp/tools/memory-tool.js +180 -0
- package/mcp/tools/orchestrator-tool.js +232 -0
- package/mcp/tools/plugin-tool.js +76 -0
- package/mcp/tools/quality-tool.js +47 -0
- package/mcp/tools/skill-tool.js +233 -0
- package/mcp/tools/telemetry-tool.js +95 -0
- package/mcp/tools/todo-tool.js +133 -0
- package/package.json +98 -0
- package/plugins/index.js +141 -0
- package/quality/index.js +380 -0
- package/quality/lint-budgets.json +19 -0
- package/skills/index.js +787 -0
- package/skills/patterns/README.md +163 -0
- package/skills/patterns/api/route-handler.md +217 -0
- package/skills/patterns/api/server-action.md +249 -0
- package/skills/patterns/auth/clerk.md +132 -0
- package/skills/patterns/database/prisma.md +180 -0
- package/skills/patterns/payments/stripe.md +272 -0
- package/skills/patterns/security/validation.md +268 -0
- package/skills/patterns/testing/vitest.md +307 -0
- package/templates/bootspring.config.js +83 -0
- package/templates/mcp.json +9 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
# Backend Expert Agent
|
|
2
|
+
|
|
3
|
+
## Role
|
|
4
|
+
Specialized in server-side logic, API design, Server Actions, business logic implementation, and backend architecture for Next.js applications.
|
|
5
|
+
|
|
6
|
+
## Core Expertise
|
|
7
|
+
|
|
8
|
+
### Server Actions
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
// app/actions/user-actions.ts
|
|
12
|
+
'use server';
|
|
13
|
+
|
|
14
|
+
import { auth } from '@clerk/nextjs/server';
|
|
15
|
+
import { prisma } from '@/lib/prisma';
|
|
16
|
+
import { revalidatePath } from 'next/cache';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
|
|
19
|
+
// Schema for input validation
|
|
20
|
+
const UpdateProfileSchema = z.object({
|
|
21
|
+
name: z.string().min(1).max(100),
|
|
22
|
+
bio: z.string().max(500).optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Type-safe action with error handling
|
|
26
|
+
export async function updateProfile(formData: FormData) {
|
|
27
|
+
// 1. Authenticate
|
|
28
|
+
const { userId } = await auth();
|
|
29
|
+
if (!userId) {
|
|
30
|
+
return { error: 'Unauthorized' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 2. Validate input
|
|
34
|
+
const result = UpdateProfileSchema.safeParse({
|
|
35
|
+
name: formData.get('name'),
|
|
36
|
+
bio: formData.get('bio'),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!result.success) {
|
|
40
|
+
return { error: 'Invalid input', details: result.error.flatten() };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 3. Execute business logic
|
|
44
|
+
try {
|
|
45
|
+
const user = await prisma.user.update({
|
|
46
|
+
where: { clerkId: userId },
|
|
47
|
+
data: result.data,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// 4. Revalidate affected paths
|
|
51
|
+
revalidatePath('/profile');
|
|
52
|
+
revalidatePath('/settings');
|
|
53
|
+
|
|
54
|
+
return { success: true, user };
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('Failed to update profile:', error);
|
|
57
|
+
return { error: 'Failed to update profile' };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Action with optimistic updates support
|
|
62
|
+
export async function toggleFavorite(itemId: string) {
|
|
63
|
+
const { userId } = await auth();
|
|
64
|
+
if (!userId) throw new Error('Unauthorized');
|
|
65
|
+
|
|
66
|
+
const existing = await prisma.favorite.findUnique({
|
|
67
|
+
where: {
|
|
68
|
+
userId_itemId: { userId, itemId }
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (existing) {
|
|
73
|
+
await prisma.favorite.delete({
|
|
74
|
+
where: { id: existing.id }
|
|
75
|
+
});
|
|
76
|
+
return { favorited: false };
|
|
77
|
+
} else {
|
|
78
|
+
await prisma.favorite.create({
|
|
79
|
+
data: { userId, itemId }
|
|
80
|
+
});
|
|
81
|
+
return { favorited: true };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Route Handlers (API Routes)
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// app/api/users/route.ts
|
|
90
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
91
|
+
import { auth } from '@clerk/nextjs/server';
|
|
92
|
+
import { prisma } from '@/lib/prisma';
|
|
93
|
+
import { z } from 'zod';
|
|
94
|
+
|
|
95
|
+
// GET: List users with pagination
|
|
96
|
+
export async function GET(request: NextRequest) {
|
|
97
|
+
const { searchParams } = new URL(request.url);
|
|
98
|
+
const page = parseInt(searchParams.get('page') ?? '1');
|
|
99
|
+
const limit = parseInt(searchParams.get('limit') ?? '10');
|
|
100
|
+
|
|
101
|
+
const [users, total] = await Promise.all([
|
|
102
|
+
prisma.user.findMany({
|
|
103
|
+
skip: (page - 1) * limit,
|
|
104
|
+
take: limit,
|
|
105
|
+
orderBy: { createdAt: 'desc' },
|
|
106
|
+
select: {
|
|
107
|
+
id: true,
|
|
108
|
+
name: true,
|
|
109
|
+
email: true,
|
|
110
|
+
createdAt: true,
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
prisma.user.count(),
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
return NextResponse.json({
|
|
117
|
+
data: users,
|
|
118
|
+
pagination: {
|
|
119
|
+
page,
|
|
120
|
+
limit,
|
|
121
|
+
total,
|
|
122
|
+
pages: Math.ceil(total / limit),
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// POST: Create user
|
|
128
|
+
const CreateUserSchema = z.object({
|
|
129
|
+
email: z.string().email(),
|
|
130
|
+
name: z.string().min(1).max(100),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
export async function POST(request: NextRequest) {
|
|
134
|
+
const { userId } = await auth();
|
|
135
|
+
if (!userId) {
|
|
136
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const body = await request.json();
|
|
140
|
+
const result = CreateUserSchema.safeParse(body);
|
|
141
|
+
|
|
142
|
+
if (!result.success) {
|
|
143
|
+
return NextResponse.json(
|
|
144
|
+
{ error: 'Validation failed', details: result.error.flatten() },
|
|
145
|
+
{ status: 400 }
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const user = await prisma.user.create({
|
|
151
|
+
data: result.data,
|
|
152
|
+
});
|
|
153
|
+
return NextResponse.json(user, { status: 201 });
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error.code === 'P2002') {
|
|
156
|
+
return NextResponse.json(
|
|
157
|
+
{ error: 'Email already exists' },
|
|
158
|
+
{ status: 409 }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// app/api/users/[id]/route.ts
|
|
166
|
+
// Dynamic route handlers
|
|
167
|
+
export async function GET(
|
|
168
|
+
request: NextRequest,
|
|
169
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
170
|
+
) {
|
|
171
|
+
const { id } = await params;
|
|
172
|
+
|
|
173
|
+
const user = await prisma.user.findUnique({
|
|
174
|
+
where: { id },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (!user) {
|
|
178
|
+
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return NextResponse.json(user);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function PATCH(
|
|
185
|
+
request: NextRequest,
|
|
186
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
187
|
+
) {
|
|
188
|
+
const { id } = await params;
|
|
189
|
+
const { userId } = await auth();
|
|
190
|
+
if (!userId) {
|
|
191
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const body = await request.json();
|
|
195
|
+
// Validate and update...
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function DELETE(
|
|
199
|
+
request: NextRequest,
|
|
200
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
201
|
+
) {
|
|
202
|
+
const { id } = await params;
|
|
203
|
+
const { userId } = await auth();
|
|
204
|
+
if (!userId) {
|
|
205
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await prisma.user.delete({ where: { id } });
|
|
209
|
+
return new NextResponse(null, { status: 204 });
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Service Layer Pattern
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// lib/services/user-service.ts
|
|
217
|
+
import { prisma } from '@/lib/prisma';
|
|
218
|
+
import { Prisma } from '@prisma/client';
|
|
219
|
+
|
|
220
|
+
export class UserService {
|
|
221
|
+
async findById(id: string) {
|
|
222
|
+
return prisma.user.findUnique({
|
|
223
|
+
where: { id },
|
|
224
|
+
include: {
|
|
225
|
+
profile: true,
|
|
226
|
+
subscription: true,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async findByEmail(email: string) {
|
|
232
|
+
return prisma.user.findUnique({ where: { email } });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async create(data: Prisma.UserCreateInput) {
|
|
236
|
+
return prisma.user.create({ data });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async update(id: string, data: Prisma.UserUpdateInput) {
|
|
240
|
+
return prisma.user.update({
|
|
241
|
+
where: { id },
|
|
242
|
+
data: {
|
|
243
|
+
...data,
|
|
244
|
+
updatedAt: new Date(),
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async softDelete(id: string) {
|
|
250
|
+
return prisma.user.update({
|
|
251
|
+
where: { id },
|
|
252
|
+
data: { deletedAt: new Date() },
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async getWithStats(id: string) {
|
|
257
|
+
const user = await prisma.user.findUnique({
|
|
258
|
+
where: { id },
|
|
259
|
+
include: {
|
|
260
|
+
_count: {
|
|
261
|
+
select: {
|
|
262
|
+
posts: true,
|
|
263
|
+
comments: true,
|
|
264
|
+
followers: true,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
return user;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export const userService = new UserService();
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Webhook Handling
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
// app/api/webhooks/stripe/route.ts
|
|
280
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
281
|
+
import Stripe from 'stripe';
|
|
282
|
+
import { prisma } from '@/lib/prisma';
|
|
283
|
+
|
|
284
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
285
|
+
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
|
|
286
|
+
|
|
287
|
+
export async function POST(request: NextRequest) {
|
|
288
|
+
const body = await request.text();
|
|
289
|
+
const signature = request.headers.get('stripe-signature')!;
|
|
290
|
+
|
|
291
|
+
let event: Stripe.Event;
|
|
292
|
+
|
|
293
|
+
// 1. Verify webhook signature
|
|
294
|
+
try {
|
|
295
|
+
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
console.error('Webhook signature verification failed:', err);
|
|
298
|
+
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 2. Handle event types
|
|
302
|
+
try {
|
|
303
|
+
switch (event.type) {
|
|
304
|
+
case 'checkout.session.completed': {
|
|
305
|
+
const session = event.data.object as Stripe.Checkout.Session;
|
|
306
|
+
await handleCheckoutComplete(session);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
case 'customer.subscription.updated': {
|
|
311
|
+
const subscription = event.data.object as Stripe.Subscription;
|
|
312
|
+
await handleSubscriptionUpdate(subscription);
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
case 'customer.subscription.deleted': {
|
|
317
|
+
const subscription = event.data.object as Stripe.Subscription;
|
|
318
|
+
await handleSubscriptionCanceled(subscription);
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
case 'invoice.payment_failed': {
|
|
323
|
+
const invoice = event.data.object as Stripe.Invoice;
|
|
324
|
+
await handlePaymentFailed(invoice);
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
default:
|
|
329
|
+
console.log(`Unhandled event type: ${event.type}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return NextResponse.json({ received: true });
|
|
333
|
+
} catch (error) {
|
|
334
|
+
console.error('Webhook handler error:', error);
|
|
335
|
+
return NextResponse.json(
|
|
336
|
+
{ error: 'Webhook handler failed' },
|
|
337
|
+
{ status: 500 }
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
|
|
343
|
+
const userId = session.metadata?.userId;
|
|
344
|
+
if (!userId) throw new Error('Missing userId in metadata');
|
|
345
|
+
|
|
346
|
+
await prisma.user.update({
|
|
347
|
+
where: { id: userId },
|
|
348
|
+
data: {
|
|
349
|
+
stripeCustomerId: session.customer as string,
|
|
350
|
+
subscriptionStatus: 'active',
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function handleSubscriptionUpdate(subscription: Stripe.Subscription) {
|
|
356
|
+
await prisma.subscription.upsert({
|
|
357
|
+
where: { stripeSubscriptionId: subscription.id },
|
|
358
|
+
update: {
|
|
359
|
+
status: subscription.status,
|
|
360
|
+
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
|
361
|
+
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
|
362
|
+
},
|
|
363
|
+
create: {
|
|
364
|
+
stripeSubscriptionId: subscription.id,
|
|
365
|
+
stripeCustomerId: subscription.customer as string,
|
|
366
|
+
status: subscription.status,
|
|
367
|
+
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Background Jobs Pattern
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
// lib/jobs/email-job.ts
|
|
377
|
+
import { Resend } from 'resend';
|
|
378
|
+
|
|
379
|
+
const resend = new Resend(process.env.RESEND_API_KEY);
|
|
380
|
+
|
|
381
|
+
export async function sendWelcomeEmail(userId: string, email: string) {
|
|
382
|
+
try {
|
|
383
|
+
await resend.emails.send({
|
|
384
|
+
from: 'welcome@yourapp.com',
|
|
385
|
+
to: email,
|
|
386
|
+
subject: 'Welcome to Our App!',
|
|
387
|
+
react: WelcomeEmailTemplate({ userId }),
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Log successful send
|
|
391
|
+
await prisma.emailLog.create({
|
|
392
|
+
data: {
|
|
393
|
+
userId,
|
|
394
|
+
type: 'welcome',
|
|
395
|
+
status: 'sent',
|
|
396
|
+
sentAt: new Date(),
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.error('Failed to send welcome email:', error);
|
|
401
|
+
// Queue for retry
|
|
402
|
+
await prisma.emailLog.create({
|
|
403
|
+
data: {
|
|
404
|
+
userId,
|
|
405
|
+
type: 'welcome',
|
|
406
|
+
status: 'failed',
|
|
407
|
+
error: error.message,
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Error Handling
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// lib/errors.ts
|
|
418
|
+
export class AppError extends Error {
|
|
419
|
+
constructor(
|
|
420
|
+
message: string,
|
|
421
|
+
public code: string,
|
|
422
|
+
public statusCode: number = 400
|
|
423
|
+
) {
|
|
424
|
+
super(message);
|
|
425
|
+
this.name = 'AppError';
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export class NotFoundError extends AppError {
|
|
430
|
+
constructor(resource: string) {
|
|
431
|
+
super(`${resource} not found`, 'NOT_FOUND', 404);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export class UnauthorizedError extends AppError {
|
|
436
|
+
constructor(message = 'Unauthorized') {
|
|
437
|
+
super(message, 'UNAUTHORIZED', 401);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export class ValidationError extends AppError {
|
|
442
|
+
constructor(public details: Record<string, string[]>) {
|
|
443
|
+
super('Validation failed', 'VALIDATION_ERROR', 400);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Usage in route handler
|
|
448
|
+
export async function GET(request: NextRequest) {
|
|
449
|
+
try {
|
|
450
|
+
const user = await userService.findById(id);
|
|
451
|
+
if (!user) throw new NotFoundError('User');
|
|
452
|
+
return NextResponse.json(user);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
if (error instanceof AppError) {
|
|
455
|
+
return NextResponse.json(
|
|
456
|
+
{ error: error.message, code: error.code },
|
|
457
|
+
{ status: error.statusCode }
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
console.error('Unexpected error:', error);
|
|
461
|
+
return NextResponse.json(
|
|
462
|
+
{ error: 'Internal server error' },
|
|
463
|
+
{ status: 500 }
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
## Backend Checklist
|
|
470
|
+
|
|
471
|
+
- [ ] Input validated with Zod schemas
|
|
472
|
+
- [ ] Authentication checked before mutations
|
|
473
|
+
- [ ] Authorization verified for resource access
|
|
474
|
+
- [ ] Errors handled gracefully
|
|
475
|
+
- [ ] Database operations optimized
|
|
476
|
+
- [ ] Webhook signatures verified
|
|
477
|
+
- [ ] Sensitive data not logged
|
|
478
|
+
- [ ] Rate limiting on public endpoints
|
|
479
|
+
- [ ] Proper HTTP status codes used
|
|
480
|
+
- [ ] Transactions used for related operations
|
|
481
|
+
|
|
482
|
+
## Trigger Keywords
|
|
483
|
+
api, server action, route handler, backend, business logic, service, webhook, endpoint, post, get, patch, delete, mutation, query, authentication, authorization, middleware
|