@polymorphism-tech/morph-spec 4.3.4 → 4.3.5
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/.morph/.morphversion +5 -0
- package/.morph/config/agents.json +948 -0
- package/.morph/config/config.json +9 -9
- package/.morph/project/context/README.md +17 -0
- package/.morph/project/context/detection-log.md +16 -0
- package/.morph/project/standards/inferred.md +59 -0
- package/.morph/standards/ai-agents/blazor-ui.md +364 -0
- package/.morph/standards/ai-agents/production.md +415 -0
- package/.morph/standards/ai-agents/setup.md +418 -0
- package/.morph/standards/ai-agents/team-orchestration.md +479 -0
- package/.morph/standards/ai-agents/workflows.md +354 -0
- package/.morph/standards/architecture/ddd/aggregates.md +120 -0
- package/.morph/standards/architecture/ddd/entities.md +99 -0
- package/.morph/standards/architecture/ddd/value-objects.md +124 -0
- package/.morph/standards/backend/api/minimal-api.md +494 -0
- package/.morph/standards/backend/api/rest.md +492 -0
- package/.morph/standards/backend/api/validation.md +88 -0
- package/.morph/standards/backend/authentication/passkeys.md +428 -0
- package/.morph/standards/backend/database/ef-core.md +199 -0
- package/.morph/standards/backend/database/migrations.md +393 -0
- package/.morph/standards/backend/database/postgresql/database.md +352 -0
- package/.morph/standards/backend/database/repository-patterns.md +528 -0
- package/.morph/standards/backend/database/vector-search-rag.md +541 -0
- package/.morph/standards/backend/dotnet/async.md +366 -0
- package/.morph/standards/backend/dotnet/core.md +117 -0
- package/.morph/standards/backend/dotnet/di.md +439 -0
- package/.morph/standards/backend/dotnet/program-cs-checklist.md +92 -0
- package/.morph/standards/backend/integrations/asaas/asaas-api.md +216 -0
- package/.morph/standards/backend/integrations/clerk/clerk-auth.md +290 -0
- package/.morph/standards/backend/integrations/hangfire/hangfire-jobs.md +350 -0
- package/.morph/standards/backend/integrations/resend/resend-email.md +385 -0
- package/.morph/standards/context/analytics.md +96 -0
- package/.morph/standards/context/bundles.md +110 -0
- package/.morph/standards/context/priming.md +78 -0
- package/.morph/standards/core/architecture.md +185 -0
- package/.morph/standards/core/coding.md +214 -0
- package/.morph/standards/core/git-branching-strategy.md +403 -0
- package/.morph/standards/core/git.md +185 -0
- package/.morph/standards/core/testing.md +295 -0
- package/.morph/standards/data/nosql/blob-storage.md +102 -0
- package/.morph/standards/data/nosql/cache/redis.md +97 -0
- package/.morph/standards/data/nosql/cosmos-db.md +118 -0
- package/.morph/standards/data/vector-search/azure-ai-search.md +121 -0
- package/.morph/standards/data/vector-search/rag-chunking.md +104 -0
- package/.morph/standards/frontend/blazor/design-checklist.md +222 -0
- package/.morph/standards/frontend/blazor/fluent-ui-setup.md +595 -0
- package/.morph/standards/frontend/blazor/fluent-ui.md +137 -0
- package/.morph/standards/frontend/blazor/html-conversion.md +184 -0
- package/.morph/standards/frontend/blazor/lifecycle.md +195 -0
- package/.morph/standards/frontend/blazor/pitfalls.md +198 -0
- package/.morph/standards/frontend/blazor/state.md +191 -0
- package/.morph/standards/frontend/design-system/animations.md +151 -0
- package/.morph/standards/frontend/design-system/naming.md +64 -0
- package/.morph/standards/frontend/nextjs/nextjs-patterns.md +198 -0
- package/.morph/standards/infrastructure/azure/azure.md +624 -0
- package/.morph/standards/infrastructure/azure/bicep/bicep-patterns.md +422 -0
- package/.morph/standards/infrastructure/azure/devops/azure-devops-setup.md +516 -0
- package/.morph/standards/infrastructure/azure/devops/local-development.md +520 -0
- package/.morph/standards/infrastructure/azure/services/functions.md +486 -0
- package/.morph/standards/infrastructure/azure/services/service-bus.md +459 -0
- package/.morph/standards/infrastructure/azure/services/storage.md +407 -0
- package/.morph/standards/infrastructure/docker/easypanel-deploy.md +196 -0
- package/.morph/standards/infrastructure/supabase/mcp-setup.md +252 -0
- package/.morph/standards/infrastructure/supabase/supabase-auth.md +176 -0
- package/.morph/standards/infrastructure/supabase/supabase-pgvector.md +169 -0
- package/.morph/standards/infrastructure/supabase/supabase-rls.md +184 -0
- package/.morph/standards/infrastructure/supabase/supabase-storage.md +153 -0
- package/.morph/standards/integration/api/graphql.md +91 -0
- package/.morph/standards/integration/api/grpc.md +114 -0
- package/.morph/standards/integration/api/rest-design.md +95 -0
- package/.morph/standards/integration/event-driven/cqrs.md +101 -0
- package/.morph/standards/integration/event-driven/event-sourcing.md +124 -0
- package/.morph/standards/integration/event-driven/service-bus.md +95 -0
- package/.morph/standards/observability/logging.md +131 -0
- package/.morph/standards/observability/metrics.md +121 -0
- package/.morph/standards/observability/monitoring.md +114 -0
- package/.morph/standards/observability/tracing.md +132 -0
- package/.morph/standards/workflows/parallel-execution.md +112 -0
- package/.morph/standards/workflows/thread-management.md +113 -0
- package/.morph/templates/.idea/morph-templates.xml +92 -0
- package/.morph/templates/.vscode/morph-templates.code-snippets +186 -0
- package/.morph/templates/IDE-SNIPPETS.md +266 -0
- package/.morph/templates/README.md +814 -0
- package/.morph/templates/REGISTRY.json +1677 -0
- package/.morph/templates/code/dotnet/backend/repository.cs +141 -0
- package/.morph/templates/code/dotnet/backend/service.cs +139 -0
- package/.morph/templates/code/dotnet/contracts/Commands.cs +74 -0
- package/.morph/templates/code/dotnet/contracts/Entities.cs +25 -0
- package/.morph/templates/code/dotnet/contracts/Queries.cs +74 -0
- package/.morph/templates/code/dotnet/contracts/README.md +74 -0
- package/.morph/templates/code/dotnet/contracts/api-contracts.cs +173 -0
- package/.morph/templates/code/dotnet/contracts/contracts.cs +217 -0
- package/.morph/templates/code/dotnet/database/migration.cs +83 -0
- package/.morph/templates/code/dotnet/frontend/component.razor +239 -0
- package/.morph/templates/code/dotnet/jobs/agent.cs +163 -0
- package/.morph/templates/code/dotnet/jobs/job.cs +171 -0
- package/.morph/templates/code/dotnet/test.cs +239 -0
- package/.morph/templates/code/sql/rls-policy.sql +57 -0
- package/.morph/templates/code/sql/supabase-migration.sql +100 -0
- package/.morph/templates/code/sql/supabase-migration.template.sql +113 -0
- package/.morph/templates/code/typescript/contracts.ts +168 -0
- package/.morph/templates/context/CONTEXT-FEATURE.md +276 -0
- package/.morph/templates/context/CONTEXT.md +181 -0
- package/.morph/templates/docs/proposal.md +182 -0
- package/.morph/templates/docs/spec.md +149 -0
- package/.morph/templates/examples/design-system-examples.md +357 -0
- package/.morph/templates/examples/spec-examples.md +90 -0
- package/.morph/templates/feature/decisions.md +187 -0
- package/.morph/templates/feature/recap.md +146 -0
- package/.morph/templates/feature/tasks.md +199 -0
- package/.morph/templates/infrastructure/azure/Dockerfile.example +82 -0
- package/.morph/templates/infrastructure/azure/README.md +286 -0
- package/.morph/templates/infrastructure/azure/app-insights.bicep +63 -0
- package/.morph/templates/infrastructure/azure/app-service.bicep +164 -0
- package/.morph/templates/infrastructure/azure/container-app-env.bicep +49 -0
- package/.morph/templates/infrastructure/azure/container-app.bicep +156 -0
- package/.morph/templates/infrastructure/azure/deploy-checklist.md +426 -0
- package/.morph/templates/infrastructure/azure/deploy.ps1 +229 -0
- package/.morph/templates/infrastructure/azure/deploy.sh +208 -0
- package/.morph/templates/infrastructure/azure/key-vault.bicep +91 -0
- package/.morph/templates/infrastructure/azure/main.bicep +189 -0
- package/.morph/templates/infrastructure/azure/parameters.dev.json +29 -0
- package/.morph/templates/infrastructure/azure/parameters.prod.json +29 -0
- package/.morph/templates/infrastructure/azure/parameters.staging.json +29 -0
- package/.morph/templates/infrastructure/azure/sql-database.bicep +103 -0
- package/.morph/templates/infrastructure/azure/storage.bicep +106 -0
- package/.morph/templates/infrastructure/docker/Dockerfile.template +58 -0
- package/.morph/templates/infrastructure/docker/docker-compose.template.yml +67 -0
- package/.morph/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -0
- package/.morph/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -0
- package/.morph/templates/infrastructure/docker/easypanel.template.json +54 -0
- package/.morph/templates/infrastructure/github/README.md +593 -0
- package/.morph/templates/infrastructure/github/actions/azure-auth/action.yml.hbs +22 -0
- package/.morph/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -0
- package/.morph/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -0
- package/.morph/templates/infrastructure/github/workflows/deploy-azure-app-service.yml.hbs +61 -0
- package/.morph/templates/infrastructure/github/workflows/deploy-easypanel.yml.hbs +31 -0
- package/.morph/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -0
- package/.morph/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -0
- package/.morph/templates/integrations/asaas-client.cs +387 -0
- package/.morph/templates/integrations/asaas-webhook.cs +351 -0
- package/.morph/templates/integrations/azure-identity-config.cs +288 -0
- package/.morph/templates/integrations/clerk-config.cs +258 -0
- package/.morph/templates/meta-prompts/fusion/fusion-agent.md +76 -0
- package/.morph/templates/meta-prompts/fusion/fusion-aggregator.md +100 -0
- package/.morph/templates/meta-prompts/hops/hop-retry.md +78 -0
- package/.morph/templates/meta-prompts/hops/hop-validation.md +97 -0
- package/.morph/templates/meta-prompts/hops/hop-wrapper.md +36 -0
- package/.morph/templates/meta-prompts/parallel-workers/parallel-coordinator.md +113 -0
- package/.morph/templates/meta-prompts/parallel-workers/parallel-worker.md +80 -0
- package/.morph/templates/meta-prompts/squad-leaders/backend-squad.md +90 -0
- package/.morph/templates/meta-prompts/squad-leaders/frontend-squad.md +126 -0
- package/.morph/templates/meta-prompts/squad-leaders/squad-leader.md +43 -0
- package/.morph/templates/meta-prompts/validators/checkpoint-validator.md +107 -0
- package/.morph/templates/meta-prompts/validators/pre-commit-validator.md +95 -0
- package/.morph/templates/saas/subscription.cs +347 -0
- package/.morph/templates/saas/tenant.cs +338 -0
- package/.morph/templates/state.template.json +17 -0
- package/.morph/templates/ui/FluentDesignTheme.cs +149 -0
- package/.morph/templates/ui/MudTheme.cs +281 -0
- package/.morph/templates/ui/design-system.css +226 -0
- package/bin/morph-spec.js +1 -1
- package/package.json +1 -1
- package/src/commands/project/update.js +100 -13
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
# Minimal API Standards - .NET
|
|
2
|
+
|
|
3
|
+
> **Scope:** universal
|
|
4
|
+
> **Layer:** 0 (always load)
|
|
5
|
+
> **Keywords:** minimal api, dotnet, asp.net, endpoint, mapget, mappost
|
|
6
|
+
> **Load When:** always
|
|
7
|
+
|
|
8
|
+
Patterns and best practices for .NET Minimal API endpoints.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Route Handler Structure
|
|
13
|
+
|
|
14
|
+
### File Organization
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
app/api/
|
|
18
|
+
auth/
|
|
19
|
+
login/
|
|
20
|
+
route.ts # POST /api/auth/login
|
|
21
|
+
logout/
|
|
22
|
+
route.ts # POST /api/auth/logout
|
|
23
|
+
users/
|
|
24
|
+
route.ts # GET /api/users, POST /api/users
|
|
25
|
+
[id]/
|
|
26
|
+
route.ts # GET /api/users/:id, PATCH /api/users/:id
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### HTTP Methods
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// app/api/users/route.ts
|
|
33
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
34
|
+
|
|
35
|
+
// GET /api/users
|
|
36
|
+
export async function GET(request: NextRequest) {
|
|
37
|
+
const searchParams = request.nextUrl.searchParams
|
|
38
|
+
const limit = searchParams.get('limit') || '10'
|
|
39
|
+
|
|
40
|
+
// Fetch users
|
|
41
|
+
return NextResponse.json({ users: [] })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// POST /api/users
|
|
45
|
+
export async function POST(request: NextRequest) {
|
|
46
|
+
const body = await request.json()
|
|
47
|
+
|
|
48
|
+
// Create user
|
|
49
|
+
return NextResponse.json({ user: {} }, { status: 201 })
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Dynamic Routes
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// app/api/users/[id]/route.ts
|
|
57
|
+
interface RouteParams {
|
|
58
|
+
params: { id: string }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function GET(
|
|
62
|
+
request: NextRequest,
|
|
63
|
+
{ params }: RouteParams
|
|
64
|
+
) {
|
|
65
|
+
const userId = params.id
|
|
66
|
+
|
|
67
|
+
// Fetch user by ID
|
|
68
|
+
return NextResponse.json({ user: {} })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function PATCH(
|
|
72
|
+
request: NextRequest,
|
|
73
|
+
{ params }: RouteParams
|
|
74
|
+
) {
|
|
75
|
+
const userId = params.id
|
|
76
|
+
const updates = await request.json()
|
|
77
|
+
|
|
78
|
+
// Update user
|
|
79
|
+
return NextResponse.json({ user: {} })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function DELETE(
|
|
83
|
+
request: NextRequest,
|
|
84
|
+
{ params }: RouteParams
|
|
85
|
+
) {
|
|
86
|
+
const userId = params.id
|
|
87
|
+
|
|
88
|
+
// Delete user
|
|
89
|
+
return NextResponse.json(null, { status: 204 })
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Request Handling
|
|
96
|
+
|
|
97
|
+
### Query Parameters
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
export async function GET(request: NextRequest) {
|
|
101
|
+
const searchParams = request.nextUrl.searchParams
|
|
102
|
+
const page = parseInt(searchParams.get('page') || '1')
|
|
103
|
+
const limit = parseInt(searchParams.get('limit') || '10')
|
|
104
|
+
const sort = searchParams.get('sort') || 'created_at'
|
|
105
|
+
|
|
106
|
+
// Use query params
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Request Body Validation (Zod)
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { z } from 'zod'
|
|
114
|
+
|
|
115
|
+
const createUserSchema = z.object({
|
|
116
|
+
name: z.string().min(1, 'Name is required'),
|
|
117
|
+
email: z.string().email('Invalid email'),
|
|
118
|
+
age: z.number().int().positive().optional()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
export async function POST(request: NextRequest) {
|
|
122
|
+
try {
|
|
123
|
+
const body = await request.json()
|
|
124
|
+
const validatedData = createUserSchema.parse(body)
|
|
125
|
+
|
|
126
|
+
// Proceed with validated data
|
|
127
|
+
return NextResponse.json({ user: validatedData }, { status: 201 })
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (error instanceof z.ZodError) {
|
|
130
|
+
return NextResponse.json(
|
|
131
|
+
{ error: 'Validation failed', details: error.errors },
|
|
132
|
+
{ status: 400 }
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
throw error
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Headers
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
export async function GET(request: NextRequest) {
|
|
144
|
+
const authHeader = request.headers.get('authorization')
|
|
145
|
+
const contentType = request.headers.get('content-type')
|
|
146
|
+
|
|
147
|
+
// Set response headers
|
|
148
|
+
return NextResponse.json(
|
|
149
|
+
{ data: {} },
|
|
150
|
+
{
|
|
151
|
+
headers: {
|
|
152
|
+
'Cache-Control': 'max-age=3600',
|
|
153
|
+
'X-Custom-Header': 'value'
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Response Patterns
|
|
163
|
+
|
|
164
|
+
### Success Responses
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// 200 OK - Successful GET
|
|
168
|
+
return NextResponse.json({ users: [] })
|
|
169
|
+
|
|
170
|
+
// 201 Created - Successful POST
|
|
171
|
+
return NextResponse.json({ user: {} }, { status: 201 })
|
|
172
|
+
|
|
173
|
+
// 204 No Content - Successful DELETE
|
|
174
|
+
return new NextResponse(null, { status: 204 })
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Error Responses
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// 400 Bad Request
|
|
181
|
+
return NextResponse.json(
|
|
182
|
+
{ error: 'Invalid input', details: errors },
|
|
183
|
+
{ status: 400 }
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
// 401 Unauthorized
|
|
187
|
+
return NextResponse.json(
|
|
188
|
+
{ error: 'Authentication required' },
|
|
189
|
+
{ status: 401 }
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
// 403 Forbidden
|
|
193
|
+
return NextResponse.json(
|
|
194
|
+
{ error: 'Insufficient permissions' },
|
|
195
|
+
{ status: 403 }
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
// 404 Not Found
|
|
199
|
+
return NextResponse.json(
|
|
200
|
+
{ error: 'Resource not found' },
|
|
201
|
+
{ status: 404 }
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
// 500 Internal Server Error
|
|
205
|
+
return NextResponse.json(
|
|
206
|
+
{ error: 'Internal server error' },
|
|
207
|
+
{ status: 500 }
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Authentication & Authorization
|
|
214
|
+
|
|
215
|
+
### Middleware-based Auth
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// middleware.ts
|
|
219
|
+
import { createMiddlewareClient } from '@/lib/supabase/middleware'
|
|
220
|
+
import { NextResponse } from 'next/server'
|
|
221
|
+
|
|
222
|
+
export async function middleware(request: NextRequest) {
|
|
223
|
+
const res = NextResponse.next()
|
|
224
|
+
const supabase = createMiddlewareClient(request, res)
|
|
225
|
+
|
|
226
|
+
const { data: { session } } = await supabase.auth.getSession()
|
|
227
|
+
|
|
228
|
+
// Protected API routes
|
|
229
|
+
if (request.nextUrl.pathname.startsWith('/api/protected') && !session) {
|
|
230
|
+
return NextResponse.json(
|
|
231
|
+
{ error: 'Unauthorized' },
|
|
232
|
+
{ status: 401 }
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return res
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Route-level Auth
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { createRouteHandlerClient } from '@/lib/supabase/server'
|
|
244
|
+
|
|
245
|
+
export async function GET(request: NextRequest) {
|
|
246
|
+
const supabase = createRouteHandlerClient()
|
|
247
|
+
const { data: { session } } = await supabase.auth.getSession()
|
|
248
|
+
|
|
249
|
+
if (!session) {
|
|
250
|
+
return NextResponse.json(
|
|
251
|
+
{ error: 'Unauthorized' },
|
|
252
|
+
{ status: 401 }
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Proceed with authenticated request
|
|
257
|
+
const { data: profile } = await supabase
|
|
258
|
+
.from('profiles')
|
|
259
|
+
.select('*')
|
|
260
|
+
.eq('user_id', session.user.id)
|
|
261
|
+
.single()
|
|
262
|
+
|
|
263
|
+
return NextResponse.json({ profile })
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Role-based Authorization
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
const ADMIN_ROLES = ['admin', 'super_admin']
|
|
271
|
+
|
|
272
|
+
export async function DELETE(
|
|
273
|
+
request: NextRequest,
|
|
274
|
+
{ params }: { params: { id: string } }
|
|
275
|
+
) {
|
|
276
|
+
const supabase = createRouteHandlerClient()
|
|
277
|
+
const { data: { session } } = await supabase.auth.getSession()
|
|
278
|
+
|
|
279
|
+
if (!session) {
|
|
280
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check user role
|
|
284
|
+
const { data: profile } = await supabase
|
|
285
|
+
.from('profiles')
|
|
286
|
+
.select('role')
|
|
287
|
+
.eq('user_id', session.user.id)
|
|
288
|
+
.single()
|
|
289
|
+
|
|
290
|
+
if (!profile || !ADMIN_ROLES.includes(profile.role)) {
|
|
291
|
+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Proceed with admin-only operation
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Error Handling
|
|
301
|
+
|
|
302
|
+
### Centralized Error Handler
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
// lib/api/errorHandler.ts
|
|
306
|
+
import { NextResponse } from 'next/server'
|
|
307
|
+
import { PostgrestError } from '@supabase/supabase-js'
|
|
308
|
+
import { z } from 'zod'
|
|
309
|
+
|
|
310
|
+
export function handleApiError(error: unknown) {
|
|
311
|
+
console.error('API Error:', error)
|
|
312
|
+
|
|
313
|
+
// Zod validation errors
|
|
314
|
+
if (error instanceof z.ZodError) {
|
|
315
|
+
return NextResponse.json(
|
|
316
|
+
{ error: 'Validation failed', details: error.errors },
|
|
317
|
+
{ status: 400 }
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Supabase errors
|
|
322
|
+
if (isPostgrestError(error)) {
|
|
323
|
+
return NextResponse.json(
|
|
324
|
+
{ error: error.message, code: error.code },
|
|
325
|
+
{ status: 400 }
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Default error
|
|
330
|
+
return NextResponse.json(
|
|
331
|
+
{ error: 'Internal server error' },
|
|
332
|
+
{ status: 500 }
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function isPostgrestError(error: unknown): error is PostgrestError {
|
|
337
|
+
return (error as PostgrestError).code !== undefined
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Usage
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
import { handleApiError } from '@/lib/api/errorHandler'
|
|
345
|
+
|
|
346
|
+
export async function POST(request: NextRequest) {
|
|
347
|
+
try {
|
|
348
|
+
// API logic
|
|
349
|
+
return NextResponse.json({ success: true })
|
|
350
|
+
} catch (error) {
|
|
351
|
+
return handleApiError(error)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Supabase Integration
|
|
359
|
+
|
|
360
|
+
### Database Operations
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
import { createRouteHandlerClient } from '@/lib/supabase/server'
|
|
364
|
+
|
|
365
|
+
export async function GET(request: NextRequest) {
|
|
366
|
+
const supabase = createRouteHandlerClient()
|
|
367
|
+
|
|
368
|
+
const { data: users, error } = await supabase
|
|
369
|
+
.from('users')
|
|
370
|
+
.select('id, name, email')
|
|
371
|
+
.order('created_at', { ascending: false })
|
|
372
|
+
.limit(10)
|
|
373
|
+
|
|
374
|
+
if (error) {
|
|
375
|
+
return NextResponse.json({ error: error.message }, { status: 500 })
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return NextResponse.json({ users })
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Row Level Security (RLS)
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
// Supabase automatically enforces RLS based on authenticated user
|
|
386
|
+
export async function GET(request: NextRequest) {
|
|
387
|
+
const supabase = createRouteHandlerClient()
|
|
388
|
+
|
|
389
|
+
// Only returns rows the authenticated user can access
|
|
390
|
+
const { data: documents } = await supabase
|
|
391
|
+
.from('documents')
|
|
392
|
+
.select('*')
|
|
393
|
+
|
|
394
|
+
return NextResponse.json({ documents })
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Caching
|
|
401
|
+
|
|
402
|
+
### Static Data (ISR)
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
export async function GET() {
|
|
406
|
+
const data = await fetchStaticData()
|
|
407
|
+
|
|
408
|
+
return NextResponse.json(
|
|
409
|
+
{ data },
|
|
410
|
+
{
|
|
411
|
+
headers: {
|
|
412
|
+
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400'
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Opt-out of Caching
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
export const dynamic = 'force-dynamic' // Opt out of caching
|
|
423
|
+
export const revalidate = 0 // Disable ISR
|
|
424
|
+
|
|
425
|
+
export async function GET() {
|
|
426
|
+
// Always fetch fresh data
|
|
427
|
+
return NextResponse.json({ data: [] })
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## CORS Configuration
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
// app/api/public/route.ts
|
|
437
|
+
export async function GET(request: NextRequest) {
|
|
438
|
+
const response = NextResponse.json({ data: [] })
|
|
439
|
+
|
|
440
|
+
// Allow CORS
|
|
441
|
+
response.headers.set('Access-Control-Allow-Origin', '*')
|
|
442
|
+
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
|
|
443
|
+
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
|
444
|
+
|
|
445
|
+
return response
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export async function OPTIONS() {
|
|
449
|
+
return new NextResponse(null, {
|
|
450
|
+
status: 204,
|
|
451
|
+
headers: {
|
|
452
|
+
'Access-Control-Allow-Origin': '*',
|
|
453
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
454
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
|
|
455
|
+
}
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Testing API Routes
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
import { describe, it, expect } from 'vitest'
|
|
466
|
+
import { GET, POST } from './route'
|
|
467
|
+
|
|
468
|
+
describe('/api/users', () => {
|
|
469
|
+
it('should return users list', async () => {
|
|
470
|
+
const request = new Request('http://localhost:3000/api/users')
|
|
471
|
+
const response = await GET(request)
|
|
472
|
+
const data = await response.json()
|
|
473
|
+
|
|
474
|
+
expect(response.status).toBe(200)
|
|
475
|
+
expect(Array.isArray(data.users)).toBe(true)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('should create a new user', async () => {
|
|
479
|
+
const request = new Request('http://localhost:3000/api/users', {
|
|
480
|
+
method: 'POST',
|
|
481
|
+
body: JSON.stringify({ name: 'John', email: 'john@example.com' })
|
|
482
|
+
})
|
|
483
|
+
const response = await POST(request)
|
|
484
|
+
const data = await response.json()
|
|
485
|
+
|
|
486
|
+
expect(response.status).toBe(201)
|
|
487
|
+
expect(data.user).toBeDefined()
|
|
488
|
+
})
|
|
489
|
+
})
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
*API Routes Standards - MORPH-SPEC by Polymorphism Tech*
|