@mr.dj2u/knowledge 0.1.6 → 0.1.8
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/content/checklists/push-merge-loop.md +17 -17
- package/dist/content/checklists/unified-agent-bundle-validation.md +9 -9
- package/dist/content/examples/push-merge-loop.md +14 -14
- package/dist/content/examples/unified-agent-bundle-bootstrap.md +8 -8
- package/dist/content/guides/animation-performance.md +31 -1
- package/dist/content/guides/post-create-onboarding.md +140 -140
- package/dist/content/patterns/animation/animation-motion-selection.md +155 -0
- package/dist/content/patterns/api/api-routes.md +313 -313
- package/dist/content/patterns/api/error-handling.md +310 -310
- package/dist/content/patterns/database/drizzle-schema.md +279 -279
- package/dist/content/patterns/database/migrations.md +364 -364
- package/dist/content/patterns/database/query-organization.md +536 -536
- package/dist/content/patterns/database/relations.md +449 -449
- package/dist/content/patterns/deployment/build-configuration.md +440 -440
- package/dist/content/patterns/deployment/ci-cd-patterns.md +447 -447
- package/dist/content/patterns/deployment/environment-config.md +379 -379
- package/dist/content/patterns/deployment/hosting-setup.md +424 -424
- package/dist/content/patterns/project/configuration-patterns.md +459 -459
- package/dist/content/patterns/project/documentation-org.md +506 -506
- package/dist/content/patterns/project/folder-structure.md +397 -397
- package/dist/content/patterns/project/library-exports.md +464 -464
- package/dist/content/patterns/project/monorepo-structure.md +500 -500
- package/dist/content/patterns/routing/dynamic-routes.md +220 -220
- package/dist/content/patterns/routing/file-based-routing.md +185 -185
- package/dist/content/patterns/routing/route-groups.md +428 -428
- package/dist/content/patterns/state/persistence-middleware.md +520 -520
- package/dist/content/patterns/state/selector-hooks.md +537 -537
- package/dist/content/patterns/state/store-organization.md +538 -538
- package/dist/content/patterns/state/zustand-patterns.md +347 -347
- package/dist/content/patterns/styling/component-styling.md +467 -467
- package/dist/content/patterns/styling/responsive-patterns.md +397 -397
- package/dist/content/patterns/styling/theme-configuration.md +425 -425
- package/dist/content/patterns/styling/uniwind-setup.md +411 -411
- package/dist/content/prompts/continue-development.md +35 -35
- package/dist/content/prompts/fix-seo.md +29 -29
- package/dist/content/prompts/onboard-new-expo-app.md +11 -11
- package/dist/content/prompts/prepare-deploy.md +29 -29
- package/dist/content/prompts/project-research-plan.md +29 -29
- package/dist/content/prompts/push-merge-loop.md +25 -25
- package/dist/content/prompts/review-expo-project.md +29 -29
- package/dist/content/prompts/review-motion.md +58 -0
- package/dist/content/prompts/run-doctor.md +38 -38
- package/dist/content/prompts/wrap-up.md +67 -67
- package/dist/content/reference/create-expo-stack-uniwind.md +29 -29
- package/dist/content/reference/mcp-sdk-transport.md +30 -30
- package/dist/content/reference/reference-repo-evacuation.md +31 -31
- package/dist/content/resource-index.json +3 -0
- package/dist/content/skills/animation-motion.md +68 -0
- package/dist/content/skills/api-routes.md +33 -33
- package/dist/content/skills/continue-development.md +32 -32
- package/dist/content/skills/debugging.md +32 -32
- package/dist/content/skills/deployment.md +32 -32
- package/dist/content/skills/dev-server-management.md +32 -32
- package/dist/content/skills/env-vars.md +32 -32
- package/dist/content/skills/expo-router-architecture.md +33 -33
- package/dist/content/skills/expo-ssr-safety.md +32 -32
- package/dist/content/skills/plugin-creation.md +41 -41
- package/dist/content/skills/production-server-patterns.md +31 -31
- package/dist/content/skills/project-onboarding.md +31 -31
- package/dist/content/skills/research-plan-intake.md +32 -32
- package/dist/content/skills/seo-metadata.md +31 -31
- package/dist/content/skills/super-stack-startup.md +34 -34
- package/dist/content/skills/uniwind-theming.md +32 -32
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -1
- package/dist/index.js.map +1 -1
- package/dist/patterns/index.d.ts +9 -1
- package/dist/patterns/index.d.ts.map +1 -1
- package/dist/patterns/index.js +17 -0
- package/dist/patterns/index.js.map +1 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +17 -0
- package/dist/prompts/index.js.map +1 -1
- package/package.json +6 -1
|
@@ -1,314 +1,314 @@
|
|
|
1
|
-
# API Routes with Catch-All Pattern
|
|
2
|
-
|
|
3
|
-
## Description
|
|
4
|
-
|
|
5
|
-
Expo Router API routes use the `+api.ts` pattern to create server-side endpoints directly alongside client routes. The `[...segments]+api.ts` syntax creates catch-all routes that capture all path segments, enabling flexible API endpoint structures with full control over HTTP methods, CORS, and request handling.
|
|
6
|
-
|
|
7
|
-
## When to Use
|
|
8
|
-
|
|
9
|
-
**Use API routes** for:
|
|
10
|
-
- ✅ Backend endpoints served from same domain as frontend
|
|
11
|
-
- ✅ Proxy servers that forward requests upstream
|
|
12
|
-
- ✅ Server-side validation and data transformation
|
|
13
|
-
- ✅ Protected endpoints with authentication
|
|
14
|
-
- ✅ Catch-all routes for dynamic path handling
|
|
15
|
-
|
|
16
|
-
## Code Example
|
|
17
|
-
|
|
18
|
-
### Basic API Route (Single Endpoint)
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
// File: app/api/quantum-backend/[...segments]+api.ts
|
|
22
|
-
import { ExpoRequest, ExpoResponse } from 'expo-server-rendering';
|
|
23
|
-
|
|
24
|
-
type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS';
|
|
25
|
-
|
|
26
|
-
const ALLOWED_ORIGINS = [
|
|
27
|
-
'http://localhost:3000',
|
|
28
|
-
'http://localhost:8081',
|
|
29
|
-
'http://127.0.0.1:3000',
|
|
30
|
-
'http://127.0.0.1:8081',
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
function normalizeOrigin(request: ExpoRequest): string | null {
|
|
34
|
-
const origin = request.headers.get('origin');
|
|
35
|
-
if (!origin) return null;
|
|
36
|
-
|
|
37
|
-
return ALLOWED_ORIGINS.includes(origin) ? origin : null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function GET(request: ExpoRequest) {
|
|
41
|
-
const allowedOrigin = normalizeOrigin(request);
|
|
42
|
-
|
|
43
|
-
if (request.method === 'OPTIONS') {
|
|
44
|
-
return new ExpoResponse(null, {
|
|
45
|
-
status: 204,
|
|
46
|
-
headers: allowedOrigin
|
|
47
|
-
? { 'Access-Control-Allow-Origin': allowedOrigin }
|
|
48
|
-
: {},
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Handle GET request
|
|
53
|
-
return new ExpoResponse(JSON.stringify({ message: 'GET OK' }), {
|
|
54
|
-
status: 200,
|
|
55
|
-
headers: {
|
|
56
|
-
'Content-Type': 'application/json',
|
|
57
|
-
...(allowedOrigin && {
|
|
58
|
-
'Access-Control-Allow-Origin': allowedOrigin,
|
|
59
|
-
}),
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function POST(request: ExpoRequest) {
|
|
65
|
-
const allowedOrigin = normalizeOrigin(request);
|
|
66
|
-
const body = await request.json();
|
|
67
|
-
|
|
68
|
-
// Process request
|
|
69
|
-
const result = {
|
|
70
|
-
success: true,
|
|
71
|
-
data: body,
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
return new ExpoResponse(JSON.stringify(result), {
|
|
75
|
-
status: 200,
|
|
76
|
-
headers: {
|
|
77
|
-
'Content-Type': 'application/json',
|
|
78
|
-
...(allowedOrigin && {
|
|
79
|
-
'Access-Control-Allow-Origin': allowedOrigin,
|
|
80
|
-
}),
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
**From:** DJsPortfolio/src/app/api/quantum-backend/[...segments]+api.ts
|
|
87
|
-
|
|
88
|
-
### Catch-All Route with Path Normalization
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
// File: app/public-facing/api/quantum/v1/[...segments]+api.ts
|
|
92
|
-
import { ExpoRequest, ExpoResponse } from 'expo-server-rendering';
|
|
93
|
-
|
|
94
|
-
const DISALLOWED_PREFIXES = ['/v1/keys', '/v1/ibm/profiles'];
|
|
95
|
-
const DEFAULT_UPSTREAM_BASE_URL = 'http://127.0.0.1:8000/v1';
|
|
96
|
-
|
|
97
|
-
type ProxyMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
98
|
-
|
|
99
|
-
function normalizeUpstreamBaseUrl(): string {
|
|
100
|
-
const raw = process.env.EXPO_PUBLIC_QUANTUM_API_BASE_URL?.trim();
|
|
101
|
-
const base = raw || DEFAULT_UPSTREAM_BASE_URL;
|
|
102
|
-
|
|
103
|
-
// Remove trailing slash
|
|
104
|
-
const trimmed = base.replace(/\/+$/, '');
|
|
105
|
-
|
|
106
|
-
// Ensure /v1 suffix
|
|
107
|
-
return trimmed.endsWith('/v1') ? trimmed : `${trimmed}/v1`;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function normalizeOperationPath(pathname: string): string {
|
|
111
|
-
// Remove leading/trailing slashes, lowercase
|
|
112
|
-
return `/${pathname.toLowerCase().trim().replace(/^\/+|\/+$/g, '')}`;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export async function POST(request: ExpoRequest) {
|
|
116
|
-
try {
|
|
117
|
-
// Parse segments from URL
|
|
118
|
-
const url = new URL(request.url);
|
|
119
|
-
const pathname = url.pathname;
|
|
120
|
-
|
|
121
|
-
// Extract segments after route prefix
|
|
122
|
-
const segments = pathname
|
|
123
|
-
.replace('/public-facing/api/quantum/v1', '')
|
|
124
|
-
.split('/')
|
|
125
|
-
.filter(Boolean);
|
|
126
|
-
|
|
127
|
-
const operationPath = '/' + segments.join('/');
|
|
128
|
-
|
|
129
|
-
// Validate disallowed paths
|
|
130
|
-
for (const prefix of DISALLOWED_PREFIXES) {
|
|
131
|
-
if (operationPath.startsWith(prefix)) {
|
|
132
|
-
return new ExpoResponse(
|
|
133
|
-
JSON.stringify({ error: 'Path not allowed' }),
|
|
134
|
-
{ status: 403, headers: { 'Content-Type': 'application/json' } }
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Forward to upstream
|
|
140
|
-
const upstreamUrl = normalizeUpstreamBaseUrl() + operationPath;
|
|
141
|
-
const upstreamRequest = new Request(upstreamUrl, {
|
|
142
|
-
method: 'POST',
|
|
143
|
-
headers: request.headers,
|
|
144
|
-
body: await request.text(),
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const response = await fetch(upstreamRequest);
|
|
148
|
-
return new ExpoResponse(await response.text(), {
|
|
149
|
-
status: response.status,
|
|
150
|
-
headers: Object.fromEntries(response.headers),
|
|
151
|
-
});
|
|
152
|
-
} catch (error) {
|
|
153
|
-
return new ExpoResponse(
|
|
154
|
-
JSON.stringify({ error: 'Internal Server Error' }),
|
|
155
|
-
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
**From:** DJsPortfolio/src/app/public-facing/api/quantum/v1/[...segments]+api.ts (lines 1-50)
|
|
162
|
-
|
|
163
|
-
## Configuration
|
|
164
|
-
|
|
165
|
-
### Enable API Routes in app.json
|
|
166
|
-
|
|
167
|
-
```json
|
|
168
|
-
{
|
|
169
|
-
"expo": {
|
|
170
|
-
"plugins": [
|
|
171
|
-
[
|
|
172
|
-
"expo-router",
|
|
173
|
-
{
|
|
174
|
-
"apiRoutes": true,
|
|
175
|
-
"origin": false
|
|
176
|
-
}
|
|
177
|
-
]
|
|
178
|
-
]
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
### Environment Variables for API Base URLs
|
|
184
|
-
|
|
185
|
-
```bash
|
|
186
|
-
# .env or .env.production
|
|
187
|
-
EXPO_PUBLIC_QUANTUM_API_BASE_URL=http://127.0.0.1:8000/v1
|
|
188
|
-
QUANTUM_UPSTREAM_API_SECRET=your-secret-key
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### TypeScript Setup
|
|
192
|
-
|
|
193
|
-
```typescript
|
|
194
|
-
import { ExpoRequest, ExpoResponse } from 'expo-server-rendering';
|
|
195
|
-
|
|
196
|
-
export type { ExpoRequest, ExpoResponse };
|
|
197
|
-
|
|
198
|
-
// Type for allowed HTTP methods
|
|
199
|
-
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
## Best Practices
|
|
203
|
-
|
|
204
|
-
### ✅ DO
|
|
205
|
-
|
|
206
|
-
1. **Always validate CORS origin** before responding
|
|
207
|
-
```typescript
|
|
208
|
-
const ALLOWED_ORIGINS = ['http://localhost:3000', 'https://app.example.com'];
|
|
209
|
-
|
|
210
|
-
function validateOrigin(origin: string | null): boolean {
|
|
211
|
-
return !!origin && ALLOWED_ORIGINS.includes(origin);
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
2. **Normalize paths and trim trailing slashes**
|
|
216
|
-
```typescript
|
|
217
|
-
function normalizePath(path: string): string {
|
|
218
|
-
return path.replace(/\/+$/, '').toLowerCase();
|
|
219
|
-
}
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
3. **Use explicit status codes**
|
|
223
|
-
```typescript
|
|
224
|
-
// ✅ GOOD
|
|
225
|
-
return new ExpoResponse(JSON.stringify(error), {
|
|
226
|
-
status: 400,
|
|
227
|
-
headers: { 'Content-Type': 'application/json' },
|
|
228
|
-
});
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
4. **Handle disallowed paths explicitly**
|
|
232
|
-
```typescript
|
|
233
|
-
const DISALLOWED_PREFIXES = ['/keys', '/credentials'];
|
|
234
|
-
|
|
235
|
-
for (const prefix of DISALLOWED_PREFIXES) {
|
|
236
|
-
if (operationPath.startsWith(prefix)) {
|
|
237
|
-
return new ExpoResponse(
|
|
238
|
-
JSON.stringify({ error: 'Forbidden' }),
|
|
239
|
-
{ status: 403 }
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
5. **Support OPTIONS preflight requests**
|
|
246
|
-
```typescript
|
|
247
|
-
if (request.method === 'OPTIONS') {
|
|
248
|
-
return new ExpoResponse(null, {
|
|
249
|
-
status: 204,
|
|
250
|
-
headers: corsHeaders,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
### ❌ DON'T
|
|
256
|
-
|
|
257
|
-
1. **Don't skip CORS validation**
|
|
258
|
-
```typescript
|
|
259
|
-
// ❌ BAD - allows any origin
|
|
260
|
-
headers: { 'Access-Control-Allow-Origin': '*' }
|
|
261
|
-
|
|
262
|
-
// ✅ GOOD - restrict to known origins
|
|
263
|
-
if (ALLOWED_ORIGINS.includes(origin)) {
|
|
264
|
-
headers: { 'Access-Control-Allow-Origin': origin }
|
|
265
|
-
}
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
2. **Don't expose sensitive environment variables**
|
|
269
|
-
```typescript
|
|
270
|
-
// ❌ BAD
|
|
271
|
-
headers: process.env.API_SECRET
|
|
272
|
-
|
|
273
|
-
// ✅ GOOD - only use server-side secrets
|
|
274
|
-
const secret = process.env.API_SECRET; // Used in headers, not exposed
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
3. **Don't pass unchecked path segments to upstream**
|
|
278
|
-
```typescript
|
|
279
|
-
// ❌ BAD - could be exploited
|
|
280
|
-
const upstreamUrl = baseUrl + segments;
|
|
281
|
-
|
|
282
|
-
// ✅ GOOD - validate and normalize
|
|
283
|
-
const validatedPath = normalizePath(segments);
|
|
284
|
-
const upstreamUrl = baseUrl + validatedPath;
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
4. **Don't forget to handle errors**
|
|
288
|
-
```typescript
|
|
289
|
-
// ❌ BAD - uncaught errors crash request
|
|
290
|
-
const data = await request.json();
|
|
291
|
-
|
|
292
|
-
// ✅ GOOD - catch and return error response
|
|
293
|
-
try {
|
|
294
|
-
const data = await request.json();
|
|
295
|
-
} catch (error) {
|
|
296
|
-
return new ExpoResponse(
|
|
297
|
-
JSON.stringify({ error: 'Invalid JSON' }),
|
|
298
|
-
{ status: 400 }
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
## Related Patterns
|
|
304
|
-
|
|
305
|
-
- [File-Based Routing](./file-based-routing.md) — Route file structure
|
|
306
|
-
- [Error Handling](../api/error-handling.md) — Custom error classes
|
|
307
|
-
- [CORS Configuration](../api/cors-configuration.md) — Origin validation
|
|
308
|
-
- [Health Endpoints](../api/health-endpoints.md) — Health check patterns
|
|
309
|
-
|
|
310
|
-
---
|
|
311
|
-
|
|
312
|
-
*Pattern extracted from production repositories: DJsPortfolio, PokePages*
|
|
313
|
-
*Files: DJsPortfolio/src\app\api\quantum-backend\[...segments]+api.ts*
|
|
1
|
+
# API Routes with Catch-All Pattern
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Expo Router API routes use the `+api.ts` pattern to create server-side endpoints directly alongside client routes. The `[...segments]+api.ts` syntax creates catch-all routes that capture all path segments, enabling flexible API endpoint structures with full control over HTTP methods, CORS, and request handling.
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
**Use API routes** for:
|
|
10
|
+
- ✅ Backend endpoints served from same domain as frontend
|
|
11
|
+
- ✅ Proxy servers that forward requests upstream
|
|
12
|
+
- ✅ Server-side validation and data transformation
|
|
13
|
+
- ✅ Protected endpoints with authentication
|
|
14
|
+
- ✅ Catch-all routes for dynamic path handling
|
|
15
|
+
|
|
16
|
+
## Code Example
|
|
17
|
+
|
|
18
|
+
### Basic API Route (Single Endpoint)
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// File: app/api/quantum-backend/[...segments]+api.ts
|
|
22
|
+
import { ExpoRequest, ExpoResponse } from 'expo-server-rendering';
|
|
23
|
+
|
|
24
|
+
type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS';
|
|
25
|
+
|
|
26
|
+
const ALLOWED_ORIGINS = [
|
|
27
|
+
'http://localhost:3000',
|
|
28
|
+
'http://localhost:8081',
|
|
29
|
+
'http://127.0.0.1:3000',
|
|
30
|
+
'http://127.0.0.1:8081',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function normalizeOrigin(request: ExpoRequest): string | null {
|
|
34
|
+
const origin = request.headers.get('origin');
|
|
35
|
+
if (!origin) return null;
|
|
36
|
+
|
|
37
|
+
return ALLOWED_ORIGINS.includes(origin) ? origin : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function GET(request: ExpoRequest) {
|
|
41
|
+
const allowedOrigin = normalizeOrigin(request);
|
|
42
|
+
|
|
43
|
+
if (request.method === 'OPTIONS') {
|
|
44
|
+
return new ExpoResponse(null, {
|
|
45
|
+
status: 204,
|
|
46
|
+
headers: allowedOrigin
|
|
47
|
+
? { 'Access-Control-Allow-Origin': allowedOrigin }
|
|
48
|
+
: {},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle GET request
|
|
53
|
+
return new ExpoResponse(JSON.stringify({ message: 'GET OK' }), {
|
|
54
|
+
status: 200,
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
...(allowedOrigin && {
|
|
58
|
+
'Access-Control-Allow-Origin': allowedOrigin,
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function POST(request: ExpoRequest) {
|
|
65
|
+
const allowedOrigin = normalizeOrigin(request);
|
|
66
|
+
const body = await request.json();
|
|
67
|
+
|
|
68
|
+
// Process request
|
|
69
|
+
const result = {
|
|
70
|
+
success: true,
|
|
71
|
+
data: body,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return new ExpoResponse(JSON.stringify(result), {
|
|
75
|
+
status: 200,
|
|
76
|
+
headers: {
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
...(allowedOrigin && {
|
|
79
|
+
'Access-Control-Allow-Origin': allowedOrigin,
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**From:** DJsPortfolio/src/app/api/quantum-backend/[...segments]+api.ts
|
|
87
|
+
|
|
88
|
+
### Catch-All Route with Path Normalization
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// File: app/public-facing/api/quantum/v1/[...segments]+api.ts
|
|
92
|
+
import { ExpoRequest, ExpoResponse } from 'expo-server-rendering';
|
|
93
|
+
|
|
94
|
+
const DISALLOWED_PREFIXES = ['/v1/keys', '/v1/ibm/profiles'];
|
|
95
|
+
const DEFAULT_UPSTREAM_BASE_URL = 'http://127.0.0.1:8000/v1';
|
|
96
|
+
|
|
97
|
+
type ProxyMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
98
|
+
|
|
99
|
+
function normalizeUpstreamBaseUrl(): string {
|
|
100
|
+
const raw = process.env.EXPO_PUBLIC_QUANTUM_API_BASE_URL?.trim();
|
|
101
|
+
const base = raw || DEFAULT_UPSTREAM_BASE_URL;
|
|
102
|
+
|
|
103
|
+
// Remove trailing slash
|
|
104
|
+
const trimmed = base.replace(/\/+$/, '');
|
|
105
|
+
|
|
106
|
+
// Ensure /v1 suffix
|
|
107
|
+
return trimmed.endsWith('/v1') ? trimmed : `${trimmed}/v1`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeOperationPath(pathname: string): string {
|
|
111
|
+
// Remove leading/trailing slashes, lowercase
|
|
112
|
+
return `/${pathname.toLowerCase().trim().replace(/^\/+|\/+$/g, '')}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function POST(request: ExpoRequest) {
|
|
116
|
+
try {
|
|
117
|
+
// Parse segments from URL
|
|
118
|
+
const url = new URL(request.url);
|
|
119
|
+
const pathname = url.pathname;
|
|
120
|
+
|
|
121
|
+
// Extract segments after route prefix
|
|
122
|
+
const segments = pathname
|
|
123
|
+
.replace('/public-facing/api/quantum/v1', '')
|
|
124
|
+
.split('/')
|
|
125
|
+
.filter(Boolean);
|
|
126
|
+
|
|
127
|
+
const operationPath = '/' + segments.join('/');
|
|
128
|
+
|
|
129
|
+
// Validate disallowed paths
|
|
130
|
+
for (const prefix of DISALLOWED_PREFIXES) {
|
|
131
|
+
if (operationPath.startsWith(prefix)) {
|
|
132
|
+
return new ExpoResponse(
|
|
133
|
+
JSON.stringify({ error: 'Path not allowed' }),
|
|
134
|
+
{ status: 403, headers: { 'Content-Type': 'application/json' } }
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Forward to upstream
|
|
140
|
+
const upstreamUrl = normalizeUpstreamBaseUrl() + operationPath;
|
|
141
|
+
const upstreamRequest = new Request(upstreamUrl, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: request.headers,
|
|
144
|
+
body: await request.text(),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const response = await fetch(upstreamRequest);
|
|
148
|
+
return new ExpoResponse(await response.text(), {
|
|
149
|
+
status: response.status,
|
|
150
|
+
headers: Object.fromEntries(response.headers),
|
|
151
|
+
});
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return new ExpoResponse(
|
|
154
|
+
JSON.stringify({ error: 'Internal Server Error' }),
|
|
155
|
+
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**From:** DJsPortfolio/src/app/public-facing/api/quantum/v1/[...segments]+api.ts (lines 1-50)
|
|
162
|
+
|
|
163
|
+
## Configuration
|
|
164
|
+
|
|
165
|
+
### Enable API Routes in app.json
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"expo": {
|
|
170
|
+
"plugins": [
|
|
171
|
+
[
|
|
172
|
+
"expo-router",
|
|
173
|
+
{
|
|
174
|
+
"apiRoutes": true,
|
|
175
|
+
"origin": false
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Environment Variables for API Base URLs
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# .env or .env.production
|
|
187
|
+
EXPO_PUBLIC_QUANTUM_API_BASE_URL=http://127.0.0.1:8000/v1
|
|
188
|
+
QUANTUM_UPSTREAM_API_SECRET=your-secret-key
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### TypeScript Setup
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { ExpoRequest, ExpoResponse } from 'expo-server-rendering';
|
|
195
|
+
|
|
196
|
+
export type { ExpoRequest, ExpoResponse };
|
|
197
|
+
|
|
198
|
+
// Type for allowed HTTP methods
|
|
199
|
+
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Best Practices
|
|
203
|
+
|
|
204
|
+
### ✅ DO
|
|
205
|
+
|
|
206
|
+
1. **Always validate CORS origin** before responding
|
|
207
|
+
```typescript
|
|
208
|
+
const ALLOWED_ORIGINS = ['http://localhost:3000', 'https://app.example.com'];
|
|
209
|
+
|
|
210
|
+
function validateOrigin(origin: string | null): boolean {
|
|
211
|
+
return !!origin && ALLOWED_ORIGINS.includes(origin);
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
2. **Normalize paths and trim trailing slashes**
|
|
216
|
+
```typescript
|
|
217
|
+
function normalizePath(path: string): string {
|
|
218
|
+
return path.replace(/\/+$/, '').toLowerCase();
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
3. **Use explicit status codes**
|
|
223
|
+
```typescript
|
|
224
|
+
// ✅ GOOD
|
|
225
|
+
return new ExpoResponse(JSON.stringify(error), {
|
|
226
|
+
status: 400,
|
|
227
|
+
headers: { 'Content-Type': 'application/json' },
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
4. **Handle disallowed paths explicitly**
|
|
232
|
+
```typescript
|
|
233
|
+
const DISALLOWED_PREFIXES = ['/keys', '/credentials'];
|
|
234
|
+
|
|
235
|
+
for (const prefix of DISALLOWED_PREFIXES) {
|
|
236
|
+
if (operationPath.startsWith(prefix)) {
|
|
237
|
+
return new ExpoResponse(
|
|
238
|
+
JSON.stringify({ error: 'Forbidden' }),
|
|
239
|
+
{ status: 403 }
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
5. **Support OPTIONS preflight requests**
|
|
246
|
+
```typescript
|
|
247
|
+
if (request.method === 'OPTIONS') {
|
|
248
|
+
return new ExpoResponse(null, {
|
|
249
|
+
status: 204,
|
|
250
|
+
headers: corsHeaders,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### ❌ DON'T
|
|
256
|
+
|
|
257
|
+
1. **Don't skip CORS validation**
|
|
258
|
+
```typescript
|
|
259
|
+
// ❌ BAD - allows any origin
|
|
260
|
+
headers: { 'Access-Control-Allow-Origin': '*' }
|
|
261
|
+
|
|
262
|
+
// ✅ GOOD - restrict to known origins
|
|
263
|
+
if (ALLOWED_ORIGINS.includes(origin)) {
|
|
264
|
+
headers: { 'Access-Control-Allow-Origin': origin }
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
2. **Don't expose sensitive environment variables**
|
|
269
|
+
```typescript
|
|
270
|
+
// ❌ BAD
|
|
271
|
+
headers: process.env.API_SECRET
|
|
272
|
+
|
|
273
|
+
// ✅ GOOD - only use server-side secrets
|
|
274
|
+
const secret = process.env.API_SECRET; // Used in headers, not exposed
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
3. **Don't pass unchecked path segments to upstream**
|
|
278
|
+
```typescript
|
|
279
|
+
// ❌ BAD - could be exploited
|
|
280
|
+
const upstreamUrl = baseUrl + segments;
|
|
281
|
+
|
|
282
|
+
// ✅ GOOD - validate and normalize
|
|
283
|
+
const validatedPath = normalizePath(segments);
|
|
284
|
+
const upstreamUrl = baseUrl + validatedPath;
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
4. **Don't forget to handle errors**
|
|
288
|
+
```typescript
|
|
289
|
+
// ❌ BAD - uncaught errors crash request
|
|
290
|
+
const data = await request.json();
|
|
291
|
+
|
|
292
|
+
// ✅ GOOD - catch and return error response
|
|
293
|
+
try {
|
|
294
|
+
const data = await request.json();
|
|
295
|
+
} catch (error) {
|
|
296
|
+
return new ExpoResponse(
|
|
297
|
+
JSON.stringify({ error: 'Invalid JSON' }),
|
|
298
|
+
{ status: 400 }
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Related Patterns
|
|
304
|
+
|
|
305
|
+
- [File-Based Routing](./file-based-routing.md) — Route file structure
|
|
306
|
+
- [Error Handling](../api/error-handling.md) — Custom error classes
|
|
307
|
+
- [CORS Configuration](../api/cors-configuration.md) — Origin validation
|
|
308
|
+
- [Health Endpoints](../api/health-endpoints.md) — Health check patterns
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
*Pattern extracted from production repositories: DJsPortfolio, PokePages*
|
|
313
|
+
*Files: DJsPortfolio/src\app\api\quantum-backend\[...segments]+api.ts*
|
|
314
314
|
*Lines 1-40 of catch-all route pattern with path normalization*
|