@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/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 server = await createNextServer<AppContext>({
130
- ports: {},
131
- routes,
132
- context: async ({ req }) => {
133
- // DEMO ONLY: this reads an unauthenticated header to simulate identity.
134
- // Real applications should verify a signed token or session cookie first.
135
- return {
136
- userId: req.headers.get("x-user-id") || "anonymous",
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 requires literal named exports for each HTTP method, so assign the
153
- central `server.api` handler to every verb your API should accept:
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 { server } from "@/server";
158
-
159
- export const DELETE = server.api;
160
- export const GET = server.api;
161
- export const HEAD = server.api;
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 { server } from "@/server";
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
- export const GET = server.route(downloadFile).handle(async () =>
218
- new Response(await loadFile(), {
219
- headers: { "Content-Type": "application/octet-stream" },
220
- }),
221
- );
216
+ import { getServer } from "@/server";
222
217
 
223
- export const GET = server.route(robotsTxt).handle(async () =>
224
- new Response("User-agent: *\nAllow: /\n", {
225
- headers: { "Content-Type": "text/plain; charset=utf-8" },
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
- export const POST = server.route(startCheckout).handle(async () =>
230
- Response.redirect("https://checkout.example.com/session/123", 303),
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
- #### `server.api`
270
+ #### `createApiRoute(getServer)`
266
271
 
267
- A Next.js handler for routes registered in `server/index.ts`. Framework-style
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 { server } from "@/server";
273
-
274
- export const DELETE = server.api;
275
- export const GET = server.api;
276
- export const HEAD = server.api;
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, so assign
287
- the same `server.api` handler to every verb your API should accept.
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 `server.api` handler.
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 { server } from "@/server";
304
+ import { getServer } from "@/server";
303
305
  import { downloadReport } from "@/features/reports/contracts";
304
306
 
305
- export const GET = server
306
- .route(downloadReport)
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 { server } from "@/server";
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 { server } from "./index";
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 server = await createNextServer({
439
- ports: {},
440
- hooks: [logging],
441
- context: async () => ({}),
442
- mapUnhandledError: () => ({
443
- status: 500,
444
- body: {
445
- code: "INTERNAL_SERVER_ERROR",
446
- message: "Internal server error",
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 { server } from "@/server";
474
+ import { contracts } from "@/server/routes";
463
475
 
464
- export const GET = createOpenAPIHandler(server.contracts, {
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
- `server.contracts` is populated from contracts registered through
472
- `createNextServer({ routes })`. If you export per-file Next handlers with
473
- `server.route(contract).handle(...)`, keep an explicit contract list or exported
474
- route registry for OpenAPI because those route files are not imported by the
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 { server } from "@/server";
510
+ import { getServer } from "@/server";
499
511
 
500
- export const { GET, HEAD } = createStorageRoute(server.ports.storage, {
501
- basePath: "/storage",
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 { server } from "@/server";
539
+ import { getServer } from "@/server";
525
540
 
526
- const uploadRouter = createUploadRouter({
527
- uploads: postUploads,
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
- export const { POST } = createUploadRoute(uploadRouter);
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 preserve the raw request body before app code runs.
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 { server } from "@/server";
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
- Verification failures return 400 so providers do not treat an invalid signature
575
- as a fulfilled event. Handler failures return 500 so at-least-once webhook
576
- providers can retry.
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, drains one
593
- bounded batch with `@beignet/core/outbox`, records a drain summary into the
594
- instrumentation port resolved from `ctx.ports` (`ports.instrumentation`, then
595
- `ports.devtools`), passes request correlation fields to outbox
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 { server } from "@/server";
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, creates app
635
- context with `server.createContextFromNext()`, runs the schedule with the
636
- inline runner from `@beignet/core/schedules`, and records `schedule` events
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 { server } from "@/server";
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 expose `server.api` or focused helpers such as OpenAPI, uploads, storage, devtools, and outbox drains.
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 { server } from "@/server";
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 server = await createNextServer({
794
- ports: {},
795
- providers: [
796
- drizzleSqliteProvider,
797
- loggerPinoProvider,
798
- ],
799
- providerEnv: process.env,
800
- context: async ({ ports }) => ({
801
- // Access providers via ports
802
- db: ports.db,
803
- logger: ports.logger,
804
- }),
805
- mapUnhandledError: () => ({
806
- status: 500,
807
- body: {
808
- code: "INTERNAL_SERVER_ERROR",
809
- message: "Internal server error",
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
- export const server = await createNextServer({
821
- ports: {},
822
- context: async () => ({}),
823
- mapUnhandledError: ({ err }) => {
824
- console.error("Unhandled error:", err);
825
- return {
826
- status: 500,
827
- body: {
828
- code: "INTERNAL_SERVER_ERROR",
829
- message: "Internal server error",
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, prefer `server.contracts` or
879
- `contractsFromRoutes(routes)` so OpenAPI is generated from the same route list
880
- used by the runtime.
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, verifies
889
- the event through a webhook definition or context-aware verifier, validates the
890
- matching event payload schema, and delegates the event to your app-owned
891
- handler.
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, verifies
896
- the provider signature through `ctx.ports.payments.verifyWebhook(...)`, and
897
- delegates the normalized event to your app-owned handler. Prefer
898
- `createWebhookRoute(...)` for new non-payment webhook integrations.
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 server = await createNextServer<AppContext>({
1003
- ports: {},
1004
- routes,
1005
- context: async () => ({ todos: [] }),
1006
- mapUnhandledError: () => ({
1007
- status: 500,
1008
- body: {
1009
- code: "INTERNAL_SERVER_ERROR",
1010
- message: "Internal server error",
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 { server } from "@/server";
1019
-
1020
- export const DELETE = server.api;
1021
- export const GET = server.api;
1022
- export const HEAD = server.api;
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 server = await createNextServer({
1038
- ports: {},
1039
- context: async ({ req }) => {
1040
- const user = await getUserFromRequest(req);
1128
+ export const getServer = createNextServerLoader(() =>
1129
+ createNextServer({
1130
+ ports: {},
1131
+ context: async ({ req }) => {
1132
+ const user = await getUserFromRequest(req);
1041
1133
 
1042
- if (!user) {
1043
- throw new AuthUnauthorizedError();
1044
- }
1134
+ if (!user) {
1135
+ throw new AuthUnauthorizedError();
1136
+ }
1045
1137
 
1046
- return { user };
1047
- },
1048
- mapUnhandledError: () => {
1049
- return {
1050
- status: 500,
1051
- body: {
1052
- code: "INTERNAL_SERVER_ERROR",
1053
- message: "Internal server error",
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 { server } from "@/server";
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!