@beignet/next 0.0.25 → 0.0.27
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/CHANGELOG.md +23 -0
- package/README.md +298 -202
- package/dist/index.d.ts +91 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +128 -21
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/routes-server/SKILL.md +16 -11
- package/src/index.ts +256 -31
package/README.md
CHANGED
|
@@ -122,47 +122,46 @@ export const contracts = contractsFromRoutes(routes);
|
|
|
122
122
|
|
|
123
123
|
```typescript
|
|
124
124
|
// server/index.ts
|
|
125
|
-
import { createNextServer } from "@beignet/next";
|
|
125
|
+
import { createNextServer, createNextServerLoader } from "@beignet/next";
|
|
126
126
|
import type { AppContext } from "@/app-context";
|
|
127
127
|
import { routes } from "@/server/routes";
|
|
128
128
|
|
|
129
|
-
export const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
mapUnhandledError: () => ({
|
|
140
|
-
status: 500,
|
|
141
|
-
body: {
|
|
142
|
-
code: "INTERNAL_SERVER_ERROR",
|
|
143
|
-
message: "Internal server error",
|
|
129
|
+
export const getServer = createNextServerLoader(() =>
|
|
130
|
+
createNextServer<AppContext>({
|
|
131
|
+
ports: {},
|
|
132
|
+
routes,
|
|
133
|
+
context: async ({ req }) => {
|
|
134
|
+
// DEMO ONLY: this reads an unauthenticated header to simulate identity.
|
|
135
|
+
// Real applications should verify a signed token or session cookie first.
|
|
136
|
+
return {
|
|
137
|
+
userId: req.headers.get("x-user-id") || "anonymous",
|
|
138
|
+
};
|
|
144
139
|
},
|
|
140
|
+
mapUnhandledError: () => ({
|
|
141
|
+
status: 500,
|
|
142
|
+
body: {
|
|
143
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
144
|
+
message: "Internal server error",
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
145
147
|
}),
|
|
146
|
-
|
|
148
|
+
);
|
|
147
149
|
```
|
|
148
150
|
|
|
149
151
|
### 5. Set up routes
|
|
150
152
|
|
|
151
153
|
Expose ordinary application routes through one catch-all framework route. Next
|
|
152
|
-
App Router
|
|
153
|
-
|
|
154
|
+
App Router imports route modules during production builds, so keep server boot
|
|
155
|
+
behind the memoized `getServer` loader and expose literal named exports with
|
|
156
|
+
`createApiRoute`:
|
|
154
157
|
|
|
155
158
|
```typescript
|
|
156
159
|
// app/api/[[...path]]/route.ts
|
|
157
|
-
import {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
export const GET =
|
|
161
|
-
|
|
162
|
-
export const OPTIONS = server.api;
|
|
163
|
-
export const PATCH = server.api;
|
|
164
|
-
export const POST = server.api;
|
|
165
|
-
export const PUT = server.api;
|
|
160
|
+
import { createApiRoute } from "@beignet/next";
|
|
161
|
+
import { getServer } from "@/server";
|
|
162
|
+
|
|
163
|
+
export const { DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT } =
|
|
164
|
+
createApiRoute(getServer);
|
|
166
165
|
```
|
|
167
166
|
|
|
168
167
|
This catch-all file is Next.js adapter glue. It forwards requests to the
|
|
@@ -179,13 +178,13 @@ as webhooks, redirects, downloads, or adapter-specific glue:
|
|
|
179
178
|
```typescript
|
|
180
179
|
// app/api/webhooks/payments/route.ts
|
|
181
180
|
import { handlePaymentWebhookUseCase } from "@/features/billing/use-cases";
|
|
182
|
-
import {
|
|
181
|
+
import { getServer } from "@/server";
|
|
183
182
|
import { createPaymentWebhookRoute } from "@beignet/next";
|
|
184
183
|
|
|
185
184
|
export const runtime = "nodejs";
|
|
186
185
|
|
|
187
186
|
export const { POST } = createPaymentWebhookRoute({
|
|
188
|
-
server,
|
|
187
|
+
server: getServer,
|
|
189
188
|
handle: async ({ ctx, event }) => {
|
|
190
189
|
await handlePaymentWebhookUseCase.run({ ctx, input: event });
|
|
191
190
|
return { status: 200, body: { received: true } };
|
|
@@ -202,7 +201,7 @@ validates the typed event catalog before your app handles the event.
|
|
|
202
201
|
|
|
203
202
|
```typescript
|
|
204
203
|
export const { POST } = createWebhookRoute({
|
|
205
|
-
server,
|
|
204
|
+
server: getServer,
|
|
206
205
|
webhook: providerWebhook,
|
|
207
206
|
handle: async ({ ctx, event }) => {
|
|
208
207
|
await handleProviderEventUseCase.run({ ctx, input: event.payload });
|
|
@@ -214,21 +213,27 @@ export const { POST } = createWebhookRoute({
|
|
|
214
213
|
For downloads, plain text, and redirects, return a native web `Response`:
|
|
215
214
|
|
|
216
215
|
```typescript
|
|
217
|
-
|
|
218
|
-
new Response(await loadFile(), {
|
|
219
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
220
|
-
}),
|
|
221
|
-
);
|
|
216
|
+
import { getServer } from "@/server";
|
|
222
217
|
|
|
223
|
-
export
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
218
|
+
export async function GET(req: Request) {
|
|
219
|
+
const server = await getServer();
|
|
220
|
+
const handle = server.route(downloadFile).handle(async () =>
|
|
221
|
+
new Response(await loadFile(), {
|
|
222
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
228
225
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
226
|
+
return handle(req);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function POST(req: Request) {
|
|
230
|
+
const server = await getServer();
|
|
231
|
+
const handle = server.route(startCheckout).handle(async () =>
|
|
232
|
+
Response.redirect("https://checkout.example.com/session/123", 303),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
return handle(req);
|
|
236
|
+
}
|
|
232
237
|
```
|
|
233
238
|
|
|
234
239
|
Native `Response` instances intentionally bypass JSON serialization and
|
|
@@ -262,29 +267,26 @@ Creates a Next.js server instance with the given options.
|
|
|
262
267
|
|
|
263
268
|
### NextServer methods
|
|
264
269
|
|
|
265
|
-
#### `
|
|
270
|
+
#### `createApiRoute(getServer)`
|
|
266
271
|
|
|
267
|
-
A Next.js
|
|
268
|
-
apps usually expose it once from a catch-all API route.
|
|
272
|
+
A Next.js catch-all route helper for routes registered in `server/index.ts`.
|
|
273
|
+
Framework-style apps usually expose it once from a catch-all API route.
|
|
269
274
|
|
|
270
275
|
```typescript
|
|
271
276
|
// app/api/[[...path]]/route.ts
|
|
272
|
-
import {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
export const GET =
|
|
276
|
-
|
|
277
|
-
export const OPTIONS = server.api;
|
|
278
|
-
export const PATCH = server.api;
|
|
279
|
-
export const POST = server.api;
|
|
280
|
-
export const PUT = server.api;
|
|
277
|
+
import { createApiRoute } from "@beignet/next";
|
|
278
|
+
import { getServer } from "@/server";
|
|
279
|
+
|
|
280
|
+
export const { DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT } =
|
|
281
|
+
createApiRoute(getServer);
|
|
281
282
|
```
|
|
282
283
|
|
|
283
284
|
This route file is not a catch-all contract. Keep contract paths explicit and
|
|
284
285
|
use the file only to expose the central server handler.
|
|
285
286
|
|
|
286
|
-
Next App Router requires literal named exports for each HTTP method
|
|
287
|
-
|
|
287
|
+
Next App Router requires literal named exports for each HTTP method. The route
|
|
288
|
+
helper keeps those exports literal while deferring provider startup until a
|
|
289
|
+
request arrives.
|
|
288
290
|
|
|
289
291
|
#### `server.route(contract)`
|
|
290
292
|
|
|
@@ -292,23 +294,26 @@ Returns a route builder for focused per-file handlers such as webhooks,
|
|
|
292
294
|
redirects, downloads, or other adapter-owned endpoints. Use
|
|
293
295
|
`defineRouteGroup<AppContext>()({ ... })` plus `defineRoutes(...)` for
|
|
294
296
|
ordinary application routes; `server.route(contract).handle(...)` route files
|
|
295
|
-
are not imported by the central
|
|
297
|
+
are not imported by the central API handler.
|
|
296
298
|
|
|
297
299
|
**Returns:** Route builder with:
|
|
298
300
|
- `handle(fn)`: Create a custom handler function
|
|
299
301
|
|
|
300
302
|
```typescript
|
|
301
303
|
// app/api/reports/[id]/download/route.ts
|
|
302
|
-
import {
|
|
304
|
+
import { getServer } from "@/server";
|
|
303
305
|
import { downloadReport } from "@/features/reports/contracts";
|
|
304
306
|
|
|
305
|
-
export
|
|
306
|
-
|
|
307
|
-
.handle(async ({ ctx, path }) =>
|
|
307
|
+
export async function GET(req: Request) {
|
|
308
|
+
const server = await getServer();
|
|
309
|
+
const handle = server.route(downloadReport).handle(async ({ ctx, path }) =>
|
|
308
310
|
new Response(await ctx.ports.reports.loadBytes(path.id), {
|
|
309
311
|
headers: { "Content-Type": "application/pdf" },
|
|
310
312
|
}),
|
|
311
313
|
);
|
|
314
|
+
|
|
315
|
+
return handle(req);
|
|
316
|
+
}
|
|
312
317
|
```
|
|
313
318
|
|
|
314
319
|
#### `server.createContextFromNext()`
|
|
@@ -319,11 +324,14 @@ Creates a context object from Next.js Server Components by automatically extract
|
|
|
319
324
|
|
|
320
325
|
```typescript
|
|
321
326
|
// app/my-page/page.tsx
|
|
322
|
-
import {
|
|
327
|
+
import { getServer } from "@/server";
|
|
323
328
|
import { getTodoUseCase } from "@/features/todos/use-cases/get-todo";
|
|
324
329
|
|
|
330
|
+
export const dynamic = "force-dynamic";
|
|
331
|
+
|
|
325
332
|
export default async function MyPage() {
|
|
326
333
|
// Create context from Next.js headers and cookies
|
|
334
|
+
const server = await getServer();
|
|
327
335
|
const ctx = await server.createContextFromNext();
|
|
328
336
|
|
|
329
337
|
// Call use case directly
|
|
@@ -365,9 +373,11 @@ background work:
|
|
|
365
373
|
```typescript
|
|
366
374
|
// server/schedules.ts
|
|
367
375
|
import { createServiceActor } from "@beignet/core/ports";
|
|
368
|
-
import {
|
|
376
|
+
import { getServer } from "./index";
|
|
369
377
|
|
|
370
378
|
export async function createScheduleContext() {
|
|
379
|
+
const server = await getServer();
|
|
380
|
+
|
|
371
381
|
return server.createServiceContext({
|
|
372
382
|
actor: createServiceActor("beignet-schedule"),
|
|
373
383
|
});
|
|
@@ -427,7 +437,7 @@ export const GET = server
|
|
|
427
437
|
Hooks can be added at the server level:
|
|
428
438
|
|
|
429
439
|
```typescript
|
|
430
|
-
import { createNextServer } from "@beignet/next";
|
|
440
|
+
import { createNextServer, createNextServerLoader } from "@beignet/next";
|
|
431
441
|
import { createLoggingHooks } from "@beignet/core/server";
|
|
432
442
|
|
|
433
443
|
const logging = createLoggingHooks({
|
|
@@ -435,18 +445,20 @@ const logging = createLoggingHooks({
|
|
|
435
445
|
requestIdHeader: "x-request-id",
|
|
436
446
|
});
|
|
437
447
|
|
|
438
|
-
export const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
+
export const getServer = createNextServerLoader(() =>
|
|
449
|
+
createNextServer({
|
|
450
|
+
ports: {},
|
|
451
|
+
hooks: [logging],
|
|
452
|
+
context: async () => ({}),
|
|
453
|
+
mapUnhandledError: () => ({
|
|
454
|
+
status: 500,
|
|
455
|
+
body: {
|
|
456
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
457
|
+
message: "Internal server error",
|
|
458
|
+
},
|
|
459
|
+
}),
|
|
448
460
|
}),
|
|
449
|
-
|
|
461
|
+
);
|
|
450
462
|
```
|
|
451
463
|
|
|
452
464
|
## OpenAPI documentation
|
|
@@ -459,20 +471,20 @@ docs; request-origin inference is available only when you opt in.
|
|
|
459
471
|
// app/api/openapi/route.ts
|
|
460
472
|
import { createOpenAPIHandler } from "@beignet/next";
|
|
461
473
|
import { env } from "@/lib/env";
|
|
462
|
-
import {
|
|
474
|
+
import { contracts } from "@/server/routes";
|
|
463
475
|
|
|
464
|
-
export const GET = createOpenAPIHandler(
|
|
476
|
+
export const GET = createOpenAPIHandler(contracts, {
|
|
465
477
|
title: "My API",
|
|
466
478
|
version: "1.0.0",
|
|
467
479
|
servers: [{ url: env.APP_URL }],
|
|
468
480
|
});
|
|
469
481
|
```
|
|
470
482
|
|
|
471
|
-
`
|
|
472
|
-
|
|
473
|
-
`server.route(contract).handle(...)`,
|
|
474
|
-
|
|
475
|
-
server automatically.
|
|
483
|
+
Export `contracts = contractsFromRoutes(routes)` from `server/routes.ts` so the
|
|
484
|
+
OpenAPI route can stay static and avoid booting providers during Next builds.
|
|
485
|
+
If you export per-file Next handlers with `server.route(contract).handle(...)`,
|
|
486
|
+
keep an explicit contract list because those route files are not imported by
|
|
487
|
+
the server automatically.
|
|
476
488
|
|
|
477
489
|
You can also serve Swagger UI without writing the HTML route by hand:
|
|
478
490
|
|
|
@@ -495,11 +507,14 @@ objects, private objects, invalid keys, and paths outside `basePath` to 404.
|
|
|
495
507
|
```typescript
|
|
496
508
|
// app/storage/[...key]/route.ts
|
|
497
509
|
import { createStorageRoute } from "@beignet/next";
|
|
498
|
-
import {
|
|
510
|
+
import { getServer } from "@/server";
|
|
499
511
|
|
|
500
|
-
export const { GET, HEAD } = createStorageRoute(
|
|
501
|
-
|
|
502
|
-
|
|
512
|
+
export const { GET, HEAD } = createStorageRoute(
|
|
513
|
+
async () => (await getServer()).ports.storage,
|
|
514
|
+
{
|
|
515
|
+
basePath: "/storage",
|
|
516
|
+
},
|
|
517
|
+
);
|
|
503
518
|
```
|
|
504
519
|
|
|
505
520
|
Served responses preserve object `Content-Type`, `Cache-Control`,
|
|
@@ -521,16 +536,18 @@ import { resolveProviderInstrumentationPort } from "@beignet/core/providers";
|
|
|
521
536
|
import { createUploadRouter } from "@beignet/core/uploads";
|
|
522
537
|
import { createUploadRoute } from "@beignet/next";
|
|
523
538
|
import { postUploads } from "@/features/posts/uploads";
|
|
524
|
-
import {
|
|
539
|
+
import { getServer } from "@/server";
|
|
525
540
|
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
ctx: () => server.createContextFromNext(),
|
|
529
|
-
storage: server.ports.storage,
|
|
530
|
-
instrumentation: resolveProviderInstrumentationPort(server.ports),
|
|
531
|
-
});
|
|
541
|
+
export const { POST } = createUploadRoute(async () => {
|
|
542
|
+
const server = await getServer();
|
|
532
543
|
|
|
533
|
-
|
|
544
|
+
return createUploadRouter({
|
|
545
|
+
uploads: postUploads,
|
|
546
|
+
ctx: () => server.createContextFromNext(),
|
|
547
|
+
storage: server.ports.storage,
|
|
548
|
+
instrumentation: resolveProviderInstrumentationPort(server.ports),
|
|
549
|
+
});
|
|
550
|
+
});
|
|
534
551
|
```
|
|
535
552
|
|
|
536
553
|
The `action` segment must be `prepare`, `upload`, or `complete`.
|
|
@@ -540,14 +557,19 @@ The `action` segment must be `prepare`, `upload`, or `complete`.
|
|
|
540
557
|
Use `createPaymentWebhookRoute(...)` for billing flows backed by
|
|
541
558
|
`ctx.ports.payments`. Use `createWebhookRoute(...)` for generic inbound
|
|
542
559
|
webhooks backed by a `defineWebhook(...)` catalog and a provider verifier. Both
|
|
543
|
-
helpers
|
|
560
|
+
helpers read the raw request body first, then build app context from the real
|
|
561
|
+
request with `server.createRequestContext(...)`, so signature verification
|
|
562
|
+
never races the context factory over one body stream and trace headers
|
|
563
|
+
propagate into context creation. The `server` option accepts any object
|
|
564
|
+
exposing `createRequestContext` — a `NextServer`, a core `ServerInstance`, or
|
|
565
|
+
a test fake — so `server: getServer` keeps working unchanged.
|
|
544
566
|
|
|
545
567
|
```typescript
|
|
546
568
|
// app/api/webhooks/github/route.ts
|
|
547
569
|
import { githubWebhook } from "@/features/integrations/webhooks";
|
|
548
570
|
import { handleGitHubWebhookUseCase } from "@/features/integrations/use-cases";
|
|
549
571
|
import { env } from "@/lib/env";
|
|
550
|
-
import {
|
|
572
|
+
import { getServer } from "@/server";
|
|
551
573
|
import { createWebhookRoute } from "@beignet/next";
|
|
552
574
|
import { createGitHubWebhookVerifier } from "@beignet/provider-webhooks-github";
|
|
553
575
|
|
|
@@ -558,7 +580,7 @@ const githubWebhookVerifier = createGitHubWebhookVerifier({
|
|
|
558
580
|
});
|
|
559
581
|
|
|
560
582
|
export const { POST } = createWebhookRoute({
|
|
561
|
-
server,
|
|
583
|
+
server: getServer,
|
|
562
584
|
webhook: githubWebhook,
|
|
563
585
|
verify: ({ input }) => githubWebhookVerifier.verify(input),
|
|
564
586
|
handle: async ({ ctx, event }) => {
|
|
@@ -571,9 +593,10 @@ export const { POST } = createWebhookRoute({
|
|
|
571
593
|
});
|
|
572
594
|
```
|
|
573
595
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
596
|
+
Body read failures return 400 before context creation runs. Verification
|
|
597
|
+
failures return 400 so providers do not treat an invalid signature as a
|
|
598
|
+
fulfilled event. Context creation failures return 500. Handler failures return
|
|
599
|
+
500 so at-least-once webhook providers can retry.
|
|
577
600
|
|
|
578
601
|
Use provider verifiers such as `createGitHubWebhookVerifier(...)` from
|
|
579
602
|
`@beignet/provider-webhooks-github` or `createStripeWebhookVerifier(...)` from
|
|
@@ -589,23 +612,24 @@ billing flows and the route generated by `beignet make payments`.
|
|
|
589
612
|
## Outbox drain routes
|
|
590
613
|
|
|
591
614
|
Use `createOutboxDrainRoute` to expose durable outbox delivery from a cron or
|
|
592
|
-
scheduled serverless route. The helper requires a bearer secret,
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
615
|
+
scheduled serverless route. The helper requires a bearer secret, builds app
|
|
616
|
+
context from the real request with `server.createRequestContext(...)`, drains
|
|
617
|
+
one bounded batch with `@beignet/core/outbox`, records a drain summary into
|
|
618
|
+
the instrumentation port resolved from `ctx.ports` (`ports.instrumentation`,
|
|
619
|
+
then `ports.devtools`), passes request correlation fields to outbox
|
|
596
620
|
instrumentation, and returns a JSON summary.
|
|
597
621
|
|
|
598
622
|
```typescript
|
|
599
623
|
// app/api/cron/outbox/drain/route.ts
|
|
600
624
|
import { createOutboxDrainRoute } from "@beignet/next";
|
|
601
625
|
import { env } from "@/lib/env";
|
|
602
|
-
import {
|
|
626
|
+
import { getServer } from "@/server";
|
|
603
627
|
import { outboxRegistry } from "@/server/outbox";
|
|
604
628
|
|
|
605
629
|
export const runtime = "nodejs";
|
|
606
630
|
|
|
607
631
|
export const { GET, POST } = createOutboxDrainRoute({
|
|
608
|
-
server,
|
|
632
|
+
server: getServer,
|
|
609
633
|
registry: outboxRegistry,
|
|
610
634
|
secret: env.CRON_SECRET,
|
|
611
635
|
batchSize: 100,
|
|
@@ -631,9 +655,10 @@ serverless apps.
|
|
|
631
655
|
## Schedule trigger routes
|
|
632
656
|
|
|
633
657
|
Use `createScheduleRoute` to trigger one registered schedule from a cron or
|
|
634
|
-
scheduled serverless route. The helper requires a bearer secret,
|
|
635
|
-
context with `server.
|
|
636
|
-
inline runner from `@beignet/core/schedules`, and
|
|
658
|
+
scheduled serverless route. The helper requires a bearer secret, builds app
|
|
659
|
+
context from the real request with `server.createRequestContext(...)`, runs
|
|
660
|
+
the schedule with the inline runner from `@beignet/core/schedules`, and
|
|
661
|
+
records `schedule` events
|
|
637
662
|
through the instrumentation port resolved from `ctx.ports`
|
|
638
663
|
(`ports.instrumentation`, then `ports.devtools`) with the request's
|
|
639
664
|
correlation fields.
|
|
@@ -642,13 +667,13 @@ correlation fields.
|
|
|
642
667
|
// app/api/cron/digests/daily-digest/route.ts
|
|
643
668
|
import { createScheduleRoute } from "@beignet/next";
|
|
644
669
|
import { env } from "@/lib/env";
|
|
645
|
-
import {
|
|
670
|
+
import { getServer } from "@/server";
|
|
646
671
|
import { schedules } from "@/server/schedules";
|
|
647
672
|
|
|
648
673
|
export const runtime = "nodejs";
|
|
649
674
|
|
|
650
675
|
export const { GET, POST } = createScheduleRoute({
|
|
651
|
-
server,
|
|
676
|
+
server: getServer,
|
|
652
677
|
schedules,
|
|
653
678
|
schedule: "digests.send-daily",
|
|
654
679
|
secret: env.CRON_SECRET,
|
|
@@ -667,6 +692,62 @@ through `ctx.ports.logger` and return a 500 so schedule providers can retry.
|
|
|
667
692
|
`source` defaults to `"next-cron-route"` and is recorded on run metadata and
|
|
668
693
|
devtools events.
|
|
669
694
|
|
|
695
|
+
## Testing route files
|
|
696
|
+
|
|
697
|
+
The webhook, payment webhook, schedule, and outbox drain route factories build
|
|
698
|
+
app context from the incoming request, not from `next/headers`, so route
|
|
699
|
+
modules execute under `bun test` with a plain `Request`. Import the route
|
|
700
|
+
module directly and call the exported handler:
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
// app/api/webhooks/github/route.test.ts
|
|
704
|
+
import { expect, it } from "bun:test";
|
|
705
|
+
import { POST } from "./route";
|
|
706
|
+
|
|
707
|
+
it("rejects unsigned webhook deliveries", async () => {
|
|
708
|
+
const response = await POST(
|
|
709
|
+
new Request("http://localhost/api/webhooks/github", {
|
|
710
|
+
method: "POST",
|
|
711
|
+
body: JSON.stringify({ action: "opened" }),
|
|
712
|
+
}),
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
expect(response.status).toBe(400);
|
|
716
|
+
});
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
To test a route factory against controlled context without booting the real
|
|
720
|
+
server, pass any object exposing `createRequestContext` as the `server`
|
|
721
|
+
option (`NextRouteServer<Ctx>`):
|
|
722
|
+
|
|
723
|
+
```typescript
|
|
724
|
+
import { expect, it } from "bun:test";
|
|
725
|
+
import { createScheduleRoute } from "@beignet/next";
|
|
726
|
+
import { schedules } from "@/server/schedules";
|
|
727
|
+
|
|
728
|
+
it("triggers the daily digest schedule", async () => {
|
|
729
|
+
const { POST } = createScheduleRoute({
|
|
730
|
+
schedules,
|
|
731
|
+
schedule: "digests.send-daily",
|
|
732
|
+
secret: "test-secret",
|
|
733
|
+
server: {
|
|
734
|
+
async createRequestContext() {
|
|
735
|
+
return createTestScheduleContext();
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
const response = await POST(
|
|
741
|
+
new Request("http://localhost/api/cron/digests/daily-digest", {
|
|
742
|
+
method: "POST",
|
|
743
|
+
headers: { authorization: "Bearer test-secret" },
|
|
744
|
+
}),
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
expect(response.status).toBe(200);
|
|
748
|
+
});
|
|
749
|
+
```
|
|
750
|
+
|
|
670
751
|
## Client creation
|
|
671
752
|
|
|
672
753
|
Use `createClient` from `@beignet/core/client` for modules under `client/` or
|
|
@@ -708,7 +789,7 @@ Beignet supports the normal App Router split:
|
|
|
708
789
|
- Server Components can call use cases directly with `server.createContextFromNext()`.
|
|
709
790
|
- Server Components can prefetch contract queries and hydrate Client Components with TanStack Query.
|
|
710
791
|
- Client Components use `createClient()` plus React Query options for interactive server state.
|
|
711
|
-
- Route handlers stay thin and
|
|
792
|
+
- Route handlers stay thin and use `createApiRoute(getServer)` or focused helpers such as OpenAPI, uploads, storage, devtools, and outbox drains.
|
|
712
793
|
|
|
713
794
|
### Server Component use-case calls
|
|
714
795
|
|
|
@@ -717,9 +798,11 @@ browser cache for the result:
|
|
|
717
798
|
|
|
718
799
|
```typescript
|
|
719
800
|
// app/posts/[slug]/page.tsx
|
|
720
|
-
import {
|
|
801
|
+
import { getServer } from "@/server";
|
|
721
802
|
import { getPostUseCase } from "@/features/posts/use-cases/get-post";
|
|
722
803
|
|
|
804
|
+
export const dynamic = "force-dynamic";
|
|
805
|
+
|
|
723
806
|
type PageProps = {
|
|
724
807
|
params: Promise<{
|
|
725
808
|
slug: string;
|
|
@@ -728,6 +811,7 @@ type PageProps = {
|
|
|
728
811
|
|
|
729
812
|
export default async function Page({ params }: PageProps) {
|
|
730
813
|
const { slug } = await params;
|
|
814
|
+
const server = await getServer();
|
|
731
815
|
const ctx = await server.createContextFromNext();
|
|
732
816
|
const post = await getPostUseCase.run({
|
|
733
817
|
ctx,
|
|
@@ -783,33 +867,35 @@ handlers. Await them before building contract path or query params.
|
|
|
783
867
|
Providers are service adapters that implement ports (database, cache, logger, etc.):
|
|
784
868
|
|
|
785
869
|
```typescript
|
|
786
|
-
import { createNextServer } from "@beignet/next";
|
|
870
|
+
import { createNextServer, createNextServerLoader } from "@beignet/next";
|
|
787
871
|
import { createDrizzleSqliteProvider } from "@beignet/provider-db-drizzle/sqlite";
|
|
788
872
|
import { loggerPinoProvider } from "@beignet/provider-logger-pino";
|
|
789
873
|
import * as schema from "@/db/schema";
|
|
790
874
|
|
|
791
875
|
const drizzleSqliteProvider = createDrizzleSqliteProvider({ schema });
|
|
792
876
|
|
|
793
|
-
export const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
877
|
+
export const getServer = createNextServerLoader(() =>
|
|
878
|
+
createNextServer({
|
|
879
|
+
ports: {},
|
|
880
|
+
providers: [
|
|
881
|
+
drizzleSqliteProvider,
|
|
882
|
+
loggerPinoProvider,
|
|
883
|
+
],
|
|
884
|
+
providerEnv: process.env,
|
|
885
|
+
context: async ({ ports }) => ({
|
|
886
|
+
// Access providers via ports
|
|
887
|
+
db: ports.db,
|
|
888
|
+
logger: ports.logger,
|
|
889
|
+
}),
|
|
890
|
+
mapUnhandledError: () => ({
|
|
891
|
+
status: 500,
|
|
892
|
+
body: {
|
|
893
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
894
|
+
message: "Internal server error",
|
|
895
|
+
},
|
|
896
|
+
}),
|
|
811
897
|
}),
|
|
812
|
-
|
|
898
|
+
);
|
|
813
899
|
```
|
|
814
900
|
|
|
815
901
|
## Error handling
|
|
@@ -817,20 +903,24 @@ export const server = await createNextServer({
|
|
|
817
903
|
### Global error handler
|
|
818
904
|
|
|
819
905
|
```typescript
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
}
|
|
906
|
+
import { createNextServer, createNextServerLoader } from "@beignet/next";
|
|
907
|
+
|
|
908
|
+
export const getServer = createNextServerLoader(() =>
|
|
909
|
+
createNextServer({
|
|
910
|
+
ports: {},
|
|
911
|
+
context: async () => ({}),
|
|
912
|
+
mapUnhandledError: ({ err }) => {
|
|
913
|
+
console.error("Unhandled error:", err);
|
|
914
|
+
return {
|
|
915
|
+
status: 500,
|
|
916
|
+
body: {
|
|
917
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
918
|
+
message: "Internal server error",
|
|
919
|
+
},
|
|
920
|
+
};
|
|
921
|
+
},
|
|
922
|
+
}),
|
|
923
|
+
);
|
|
834
924
|
```
|
|
835
925
|
|
|
836
926
|
`mapUnhandledError` response bodies are sent to clients. Use `onCaughtError`
|
|
@@ -875,9 +965,9 @@ Creates a Next.js route handler that returns an OpenAPI 3.1 JSON document. Requi
|
|
|
875
965
|
Pass `servers` for deployed docs. If `servers` is omitted, no OpenAPI
|
|
876
966
|
`servers` entry is generated unless `inferServersFromRequest: true` is set.
|
|
877
967
|
|
|
878
|
-
When you use central route registration,
|
|
879
|
-
`
|
|
880
|
-
|
|
968
|
+
When you use central route registration, export `contractsFromRoutes(routes)`
|
|
969
|
+
from `server/routes.ts` so OpenAPI is generated from the same route list used
|
|
970
|
+
by the runtime without booting providers during route-module import.
|
|
881
971
|
|
|
882
972
|
### `createSwaggerUIHandler(options?): (req: Request) => Response`
|
|
883
973
|
|
|
@@ -885,17 +975,20 @@ Creates a Next.js route handler that serves Swagger UI for an OpenAPI endpoint.
|
|
|
885
975
|
|
|
886
976
|
### `createWebhookRoute(options): { POST }`
|
|
887
977
|
|
|
888
|
-
Creates a generic Next.js webhook route. The route reads the raw body,
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
978
|
+
Creates a generic Next.js webhook route. The route reads the raw body, builds
|
|
979
|
+
app context from the real request with `server.createRequestContext(...)`,
|
|
980
|
+
verifies the event through a webhook definition or context-aware verifier,
|
|
981
|
+
validates the matching event payload schema, and delegates the event to your
|
|
982
|
+
app-owned handler.
|
|
892
983
|
|
|
893
984
|
### `createPaymentWebhookRoute(options): { POST }`
|
|
894
985
|
|
|
895
|
-
Creates a Next.js payment webhook route. The route reads the raw body,
|
|
896
|
-
the
|
|
897
|
-
|
|
898
|
-
`
|
|
986
|
+
Creates a Next.js payment webhook route. The route reads the raw body, builds
|
|
987
|
+
app context from the real request with `server.createRequestContext(...)`,
|
|
988
|
+
verifies the provider signature through
|
|
989
|
+
`ctx.ports.payments.verifyWebhook(...)`, and delegates the normalized event to
|
|
990
|
+
your app-owned handler. Prefer `createWebhookRoute(...)` for new non-payment
|
|
991
|
+
webhook integrations.
|
|
899
992
|
|
|
900
993
|
### `toRequestLike(req: Request): HttpRequestLike`
|
|
901
994
|
|
|
@@ -995,66 +1088,66 @@ export const contracts = contractsFromRoutes(routes);
|
|
|
995
1088
|
|
|
996
1089
|
```typescript
|
|
997
1090
|
// server/index.ts
|
|
998
|
-
import { createNextServer } from "@beignet/next";
|
|
1091
|
+
import { createNextServer, createNextServerLoader } from "@beignet/next";
|
|
999
1092
|
import type { AppContext } from "@/app-context";
|
|
1000
1093
|
import { routes } from "@/server/routes";
|
|
1001
1094
|
|
|
1002
|
-
export const
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1095
|
+
export const getServer = createNextServerLoader(() =>
|
|
1096
|
+
createNextServer<AppContext>({
|
|
1097
|
+
ports: {},
|
|
1098
|
+
routes,
|
|
1099
|
+
context: async () => ({ todos: [] }),
|
|
1100
|
+
mapUnhandledError: () => ({
|
|
1101
|
+
status: 500,
|
|
1102
|
+
body: {
|
|
1103
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
1104
|
+
message: "Internal server error",
|
|
1105
|
+
},
|
|
1106
|
+
}),
|
|
1012
1107
|
}),
|
|
1013
|
-
|
|
1108
|
+
);
|
|
1014
1109
|
```
|
|
1015
1110
|
|
|
1016
1111
|
```typescript
|
|
1017
1112
|
// app/api/[[...path]]/route.ts
|
|
1018
|
-
import {
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
export const GET =
|
|
1022
|
-
|
|
1023
|
-
export const OPTIONS = server.api;
|
|
1024
|
-
export const PATCH = server.api;
|
|
1025
|
-
export const POST = server.api;
|
|
1026
|
-
export const PUT = server.api;
|
|
1113
|
+
import { createApiRoute } from "@beignet/next";
|
|
1114
|
+
import { getServer } from "@/server";
|
|
1115
|
+
|
|
1116
|
+
export const { DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT } =
|
|
1117
|
+
createApiRoute(getServer);
|
|
1027
1118
|
```
|
|
1028
1119
|
|
|
1029
1120
|
### With authentication
|
|
1030
1121
|
|
|
1031
1122
|
```typescript
|
|
1032
1123
|
// server/index.ts
|
|
1033
|
-
import { createNextServer } from "@beignet/next";
|
|
1124
|
+
import { createNextServer, createNextServerLoader } from "@beignet/next";
|
|
1034
1125
|
import { AuthUnauthorizedError } from "@beignet/core/ports";
|
|
1035
1126
|
import { getTodo } from "@/features/todos/contracts";
|
|
1036
1127
|
|
|
1037
|
-
export const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1128
|
+
export const getServer = createNextServerLoader(() =>
|
|
1129
|
+
createNextServer({
|
|
1130
|
+
ports: {},
|
|
1131
|
+
context: async ({ req }) => {
|
|
1132
|
+
const user = await getUserFromRequest(req);
|
|
1041
1133
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1134
|
+
if (!user) {
|
|
1135
|
+
throw new AuthUnauthorizedError();
|
|
1136
|
+
}
|
|
1045
1137
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
})
|
|
1138
|
+
return { user };
|
|
1139
|
+
},
|
|
1140
|
+
mapUnhandledError: () => {
|
|
1141
|
+
return {
|
|
1142
|
+
status: 500,
|
|
1143
|
+
body: {
|
|
1144
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
1145
|
+
message: "Internal server error",
|
|
1146
|
+
},
|
|
1147
|
+
};
|
|
1148
|
+
},
|
|
1149
|
+
}),
|
|
1150
|
+
);
|
|
1058
1151
|
```
|
|
1059
1152
|
|
|
1060
1153
|
### Server component usage
|
|
@@ -1063,11 +1156,14 @@ You can call use cases directly from React Server Components using `createContex
|
|
|
1063
1156
|
|
|
1064
1157
|
```typescript
|
|
1065
1158
|
// app/todos/page.tsx
|
|
1066
|
-
import {
|
|
1159
|
+
import { getServer } from "@/server";
|
|
1067
1160
|
import { listTodosUseCase } from "@/features/todos/use-cases/list-todos";
|
|
1068
1161
|
|
|
1162
|
+
export const dynamic = "force-dynamic";
|
|
1163
|
+
|
|
1069
1164
|
export default async function TodosPage() {
|
|
1070
1165
|
// Create context from Next.js runtime
|
|
1166
|
+
const server = await getServer();
|
|
1071
1167
|
const ctx = await server.createContextFromNext();
|
|
1072
1168
|
|
|
1073
1169
|
// Call use case directly - no API route needed!
|