@beignet/next 0.0.1

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 ADDED
@@ -0,0 +1,725 @@
1
+ # @beignet/next
2
+
3
+ Next.js adapter for the framework-agnostic `@beignet/core/server` runtime. It translates Next's `Request`/`Response` to/from the runtime's `HttpRequestLike`/`HttpResponseLike` shapes and provides small helpers for App Router handlers, OpenAPI routes, Swagger UI, and client base URLs.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @beignet/next next
9
+ ```
10
+
11
+ ### Optional add-ons
12
+
13
+ - `@beignet/core/openapi` for OpenAPI documentation
14
+ - `@beignet/core/ports` if you want to define shared ports explicitly in your app
15
+
16
+ ## TypeScript requirements
17
+
18
+ This package requires TypeScript 5.0 or higher for proper type inference.
19
+
20
+ ## Quick start
21
+
22
+ ### 1. Define your contracts
23
+
24
+ ```typescript
25
+ // features/todos/contracts.ts
26
+ import { createContractGroup } from "@beignet/core/contracts";
27
+ import { z } from "zod";
28
+
29
+ const todos = createContractGroup()
30
+ .namespace("todos")
31
+ .prefix("/api/todos");
32
+
33
+ export const getTodo = todos
34
+ .get("/:id")
35
+ .pathParams(z.object({ id: z.string() }))
36
+ .responses({ 200: z.object({
37
+ id: z.string(),
38
+ title: z.string(),
39
+ completed: z.boolean(),
40
+ }) });
41
+ ```
42
+
43
+ ### 2. Create your server
44
+
45
+ ```typescript
46
+ // server/index.ts
47
+ import {
48
+ createNextServer,
49
+ defineRouteGroup,
50
+ defineRoutes,
51
+ } from "@beignet/next";
52
+ import { getTodo } from "@/features/todos/contracts";
53
+
54
+ export const server = await createNextServer({
55
+ ports: {},
56
+ routes: defineRoutes([
57
+ {
58
+ contract: getTodo,
59
+ handle: async ({ path }) => ({
60
+ status: 200,
61
+ body: {
62
+ id: path.id,
63
+ title: "Example todo",
64
+ completed: false,
65
+ },
66
+ }),
67
+ },
68
+ ]),
69
+ createContext: async ({ req }) => {
70
+ // DEMO ONLY: this reads an unauthenticated header to simulate identity.
71
+ // Real applications should verify a signed token or session cookie first.
72
+ return {
73
+ userId: req.headers.get("x-user-id") || "anonymous",
74
+ };
75
+ },
76
+ mapUnhandledError: ({ err }) => ({
77
+ status: 500,
78
+ body: {
79
+ code: "INTERNAL_SERVER_ERROR",
80
+ message: "Internal server error",
81
+ ...(err instanceof Error ? { details: { message: err.message } } : {}),
82
+ },
83
+ }),
84
+ });
85
+ ```
86
+
87
+ For larger apps, group related handlers near the feature and compose them with
88
+ `defineRoutes`:
89
+
90
+ ```typescript
91
+ const todoRoutes = defineRouteGroup({
92
+ name: "todos",
93
+ routes: [
94
+ {
95
+ contract: getTodo,
96
+ handle: async ({ path }) => ({
97
+ status: 200,
98
+ body: {
99
+ id: path.id,
100
+ title: "Example todo",
101
+ completed: false,
102
+ },
103
+ }),
104
+ },
105
+ ],
106
+ });
107
+
108
+ export const routes = defineRoutes([todoRoutes]);
109
+ ```
110
+
111
+ ### 3. Set up routes
112
+
113
+ You have two options for routing:
114
+
115
+ #### Option A: catch-all framework route (recommended)
116
+
117
+ Register handlers centrally in `server/index.ts`, then expose the central
118
+ handler from one catch-all Next.js route file:
119
+
120
+ ```typescript
121
+ // app/api/[[...path]]/route.ts
122
+ import { server } from "@/server";
123
+
124
+ export const DELETE = server.api;
125
+ export const GET = server.api;
126
+ export const HEAD = server.api;
127
+ export const OPTIONS = server.api;
128
+ export const PATCH = server.api;
129
+ export const POST = server.api;
130
+ export const PUT = server.api;
131
+ ```
132
+
133
+ #### Option B: per-contract routes
134
+
135
+ Create individual route files for each contract:
136
+
137
+ ```typescript
138
+ // app/api/todos/[id]/route.ts
139
+ import { server } from "@/server";
140
+ import { getTodo } from "@/features/todos/contracts";
141
+
142
+ export const GET = server
143
+ .route(getTodo)
144
+ .handle(async ({ ctx, path }) => {
145
+ // Implement your handler logic
146
+ const todo = await fetchTodoById(path.id);
147
+ return {
148
+ status: 200,
149
+ body: todo,
150
+ };
151
+ });
152
+ ```
153
+
154
+ ### Raw requests and non-JSON responses
155
+
156
+ `@beignet/next` exposes the underlying web `Request` through
157
+ `HttpRequestLike`. This lets handlers read raw bodies for webhooks while keeping
158
+ the normal JSON contract flow for the rest of the app.
159
+
160
+ ```typescript
161
+ export const POST = server.route(stripeWebhook).handle(async ({ req }) => {
162
+ const rawBody = await req.text();
163
+ const signature = req.headers.get("stripe-signature");
164
+
165
+ verifyWebhookSignature(rawBody, signature);
166
+
167
+ return { status: 200, body: { received: true } };
168
+ });
169
+ ```
170
+
171
+ For downloads, plain text, and redirects, return a native web `Response`:
172
+
173
+ ```typescript
174
+ export const GET = server.route(downloadFile).handle(async () =>
175
+ new Response(await loadFile(), {
176
+ headers: { "Content-Type": "application/octet-stream" },
177
+ }),
178
+ );
179
+
180
+ export const GET = server.route(robotsTxt).handle(async () =>
181
+ new Response("User-agent: *\nAllow: /\n", {
182
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
183
+ }),
184
+ );
185
+
186
+ export const POST = server.route(startCheckout).handle(async () =>
187
+ Response.redirect("https://checkout.example.com/session/123", 303),
188
+ );
189
+ ```
190
+
191
+ Native `Response` instances intentionally bypass JSON serialization and
192
+ response schema validation. Use `{ status, body }` when you want Beignet to
193
+ validate a JSON response; use `Response` when you want full transport control.
194
+ Response-shaping hooks such as `beforeSend` only run for plain Beignet
195
+ responses; observation hooks such as `afterSend` still receive the final status
196
+ and headers.
197
+
198
+ ## API reference
199
+
200
+ ### `createNextServer<Ctx>(options)`
201
+
202
+ Creates a Next.js server instance with the given options.
203
+
204
+ **Parameters:**
205
+ - `options`: Same as `createServer` from `@beignet/core/server`:
206
+ - `ports`: **Required** - Ports object defining available service interfaces
207
+ - `createContext`: Async function to create request context
208
+ - `mapUnhandledError`: Error handler function
209
+ - `routes?`: Array of route configurations (contract + handler)
210
+ - `hooks?`: Optional ordered server hooks
211
+ - `providers?`: Optional array of service providers
212
+ - `providerEnv?`: Optional environment variables for providers
213
+ - `providerConfig?`: Optional provider configuration overrides
214
+
215
+ **Returns:** `Promise<NextServer<Ctx>>`
216
+
217
+ ### NextServer methods
218
+
219
+ #### `server.api`
220
+
221
+ A Next.js handler for routes registered in `server/index.ts`. Framework-style
222
+ apps usually expose it once from a catch-all API route.
223
+
224
+ ```typescript
225
+ // app/api/[[...path]]/route.ts
226
+ import { server } from "@/server";
227
+
228
+ export const DELETE = server.api;
229
+ export const GET = server.api;
230
+ export const HEAD = server.api;
231
+ export const OPTIONS = server.api;
232
+ export const PATCH = server.api;
233
+ export const POST = server.api;
234
+ export const PUT = server.api;
235
+ ```
236
+
237
+ #### `server.route(contract)`
238
+
239
+ Returns a route builder for creating custom handlers for a specific contract. The contract is registered globally and available via `server.api`.
240
+
241
+ **Returns:** Route builder with:
242
+ - `handle(fn)`: Create a custom handler function
243
+
244
+ ```typescript
245
+ // app/api/todos/[id]/route.ts
246
+ import { server } from "@/server";
247
+ import { getTodo } from "@/features/todos/contracts";
248
+ import { getTodoUseCase } from "@/features/todos/use-cases/get-todo";
249
+
250
+ // Option 1: Custom handler
251
+ export const GET = server
252
+ .route(getTodo)
253
+ .handle(async ({ req, ctx, path, query, body }) => {
254
+ // Your implementation
255
+ return { status: 200, body: { id: path.id, title: "..." } };
256
+ });
257
+
258
+ // Option 2: Call a use case inside the handler
259
+ export const GET = server
260
+ .route(getTodo)
261
+ .handle(async ({ ctx, path }) => {
262
+ const todo = await getTodoUseCase.run({
263
+ ctx,
264
+ input: { id: path.id },
265
+ });
266
+
267
+ return { status: 200, body: todo };
268
+ });
269
+ ```
270
+
271
+ #### `server.createContextFromNext()`
272
+
273
+ Creates a context object from Next.js Server Components by automatically extracting headers and cookies. This allows you to call use cases directly from React Server Components without going through API routes.
274
+
275
+ **Returns:** `Promise<Ctx>` - Your context object from `createContext`
276
+
277
+ ```typescript
278
+ // app/my-page/page.tsx
279
+ import { server } from "@/server";
280
+ import { getTodoUseCase } from "@/features/todos/use-cases/get-todo";
281
+
282
+ export default async function MyPage() {
283
+ // Create context from Next.js headers and cookies
284
+ const ctx = await server.createContextFromNext();
285
+
286
+ // Call use case directly
287
+ const todo = await getTodoUseCase.run({
288
+ ctx,
289
+ input: { id: "123" }
290
+ });
291
+
292
+ return <div>{todo.title}</div>;
293
+ }
294
+ ```
295
+
296
+ This method:
297
+ - Automatically calls Next.js's `headers()` and `cookies()` functions
298
+ - Creates a minimal Request-like object with headers and cookies access
299
+ - Calls your `createContext` function with this request object
300
+ - Returns the same context type you get in API route handlers
301
+ - Uses the HTTP method `"GET"` for the internal Request-like object. If your `createContext` implementation inspects `req.method`, it will always see `"GET"` when invoked via `createContextFromNext()`.
302
+ - The `req.url` is set to a placeholder (`http://core/server-component.invalid`) since Server Components don't have real HTTP URLs
303
+ - The `req.json()` and `req.text()` methods return empty values since there's no actual HTTP request body in Server Components
304
+
305
+ **Note:** This method can only be called from Next.js Server Components (not in Client Components or during build time).
306
+
307
+ #### `server.stop()`
308
+
309
+ Stops the server and cleans up resources (closes provider connections, etc.).
310
+
311
+ ```typescript
312
+ await server.stop();
313
+ ```
314
+
315
+ ## Handler function context
316
+
317
+ When using `.handle()`, your handler function receives an object with:
318
+
319
+ ```typescript
320
+ {
321
+ req: HttpRequestLike, // Raw request object
322
+ ctx: Ctx, // Your custom context from createContext
323
+ path: PathParams, // Validated path parameters
324
+ query: QueryParams, // Validated query parameters
325
+ body: Body, // Validated request body
326
+ contract: Contract, // Resolved contract metadata and schemas
327
+ }
328
+ ```
329
+
330
+ ## Use case integration
331
+
332
+ Beignet promotes clean architecture by separating use cases from HTTP concerns. Call use cases from handlers so the HTTP layer stays explicit:
333
+
334
+ ```typescript
335
+ // features/todos/use-cases/get-todo.ts
336
+ export async function getTodoUseCase(
337
+ input: { id: string },
338
+ ports: AppPorts
339
+ ) {
340
+ return await ports.db.todos.findById(input.id);
341
+ }
342
+
343
+ // app/api/todos/[id]/route.ts
344
+ export const GET = server
345
+ .route(getTodo)
346
+ .handle(async ({ ctx, path }) => {
347
+ const todo = await getTodoUseCase({ id: path.id }, ctx.ports);
348
+
349
+ return { status: 200, body: todo };
350
+ });
351
+ ```
352
+
353
+ ## Hooks
354
+
355
+ Hooks can be added at the server level:
356
+
357
+ ```typescript
358
+ import { createNextServer } from "@beignet/next";
359
+ import { createLoggingHooks } from "@beignet/core/server";
360
+
361
+ const logging = createLoggingHooks({
362
+ logger: console,
363
+ requestIdHeader: "x-request-id",
364
+ });
365
+
366
+ export const server = await createNextServer({
367
+ ports: {},
368
+ hooks: [logging],
369
+ createContext: async () => ({}),
370
+ mapUnhandledError: () => ({
371
+ status: 500,
372
+ body: {
373
+ code: "INTERNAL_SERVER_ERROR",
374
+ message: "Internal server error",
375
+ },
376
+ }),
377
+ });
378
+ ```
379
+
380
+ ## OpenAPI documentation
381
+
382
+ If you have `@beignet/core/openapi` installed, use `createOpenAPIHandler` for a Next.js route. The handler infers the current request origin and adds it as the OpenAPI server by default.
383
+
384
+ ```typescript
385
+ // app/api/openapi/route.ts
386
+ import { createOpenAPIHandler } from "@beignet/next";
387
+ import { server } from "@/server";
388
+
389
+ export const GET = createOpenAPIHandler(server.contracts, {
390
+ title: "My API",
391
+ version: "1.0.0",
392
+ });
393
+ ```
394
+
395
+ `server.contracts` is populated from contracts registered through
396
+ `createNextServer({ routes })`. If you export per-file Next handlers with
397
+ `server.route(contract).handle(...)`, keep an explicit contract list or exported
398
+ route registry for OpenAPI because those route files are not imported by the
399
+ server automatically.
400
+
401
+ You can also serve Swagger UI without writing the HTML route by hand:
402
+
403
+ ```typescript
404
+ // app/api/docs/route.ts
405
+ import { createSwaggerUIHandler } from "@beignet/next";
406
+
407
+ export const GET = createSwaggerUIHandler({
408
+ title: "My API Documentation",
409
+ specUrl: "/api/openapi",
410
+ });
411
+ ```
412
+
413
+ ## Public storage routes
414
+
415
+ Use `createStorageRoute` to serve public objects from a `StoragePort` in a
416
+ Next.js App Router route. The route streams object bodies and maps missing
417
+ objects, private objects, invalid keys, and paths outside `basePath` to 404.
418
+
419
+ ```typescript
420
+ // app/storage/[...key]/route.ts
421
+ import { createStorageRoute } from "@beignet/next";
422
+ import { server } from "@/server";
423
+
424
+ export const { GET, HEAD } = createStorageRoute(server.ports.storage, {
425
+ basePath: "/storage",
426
+ });
427
+ ```
428
+
429
+ Served responses preserve object `Content-Type`, `Cache-Control`,
430
+ `Content-Length`, and `Last-Modified` headers when available.
431
+
432
+ ## Client creation
433
+
434
+ Use `createNextClient` when a client may run in both browser and server environments. Browser calls default to same-origin relative URLs. Server calls use `NEXT_PUBLIC_API_URL`, then `VERCEL_URL`, then `http://localhost:${PORT || 3000}`.
435
+
436
+ ```typescript
437
+ // client/api-client.ts
438
+ import { createNextClient } from "@beignet/next";
439
+
440
+ export const apiClient = createNextClient({
441
+ headers: async () => ({}),
442
+ });
443
+ ```
444
+
445
+ If your local app runs on a non-default port, provide a server-only fallback:
446
+
447
+ ```typescript
448
+ export const apiClient = createNextClient({
449
+ serverBaseUrl: () => `http://localhost:${process.env.PORT || 3002}`,
450
+ });
451
+ ```
452
+
453
+ For deployed apps, prefer setting `NEXT_PUBLIC_API_URL` when API calls should target a different origin.
454
+
455
+ ## Providers
456
+
457
+ Providers are service adapters that implement ports (database, cache, logger, etc.):
458
+
459
+ ```typescript
460
+ import { createNextServer } from "@beignet/next";
461
+ import { createDrizzleTursoProvider } from "@beignet/provider-drizzle-turso";
462
+ import { loggerPinoProvider } from "@beignet/provider-logger-pino";
463
+ import * as schema from "@/db/schema";
464
+
465
+ const drizzleTursoProvider = createDrizzleTursoProvider({ schema });
466
+
467
+ export const server = await createNextServer({
468
+ ports: {},
469
+ providers: [
470
+ drizzleTursoProvider,
471
+ loggerPinoProvider,
472
+ ],
473
+ providerEnv: process.env,
474
+ createContext: async ({ ports }) => ({
475
+ // Access providers via ports
476
+ db: ports.db,
477
+ logger: ports.logger,
478
+ }),
479
+ mapUnhandledError: () => ({
480
+ status: 500,
481
+ body: {
482
+ code: "INTERNAL_SERVER_ERROR",
483
+ message: "Internal server error",
484
+ },
485
+ }),
486
+ });
487
+ ```
488
+
489
+ ## Error handling
490
+
491
+ ### Global error handler
492
+
493
+ ```typescript
494
+ export const server = await createNextServer({
495
+ ports: {},
496
+ createContext: async () => ({}),
497
+ mapUnhandledError: ({ err, ctx }) => {
498
+ console.error("Unhandled error:", err);
499
+ return {
500
+ status: 500,
501
+ body: {
502
+ code: "INTERNAL_SERVER_ERROR",
503
+ message: "Internal server error",
504
+ ...(process.env.NODE_ENV === "development" && err instanceof Error
505
+ ? { error: err.message }
506
+ : {}),
507
+ },
508
+ };
509
+ },
510
+ });
511
+ ```
512
+
513
+ ### Route-level error handling
514
+
515
+ Declare expected business failures on the contract with `.errors(...)`, then
516
+ throw your app's catalog helper from handlers or use cases.
517
+
518
+ ```typescript
519
+ import { appError } from "@/features/shared/errors";
520
+
521
+ export const GET = server
522
+ .route(getTodo)
523
+ .handle(async ({ ctx, path }) => {
524
+ const todo = await fetchTodoById(path.id);
525
+
526
+ if (!todo) {
527
+ throw appError("TodoNotFound", { details: { id: path.id } });
528
+ }
529
+
530
+ return { status: 200, body: todo };
531
+ });
532
+ ```
533
+
534
+ ## Helper functions
535
+
536
+ ### `createNextClient(config?): Client`
537
+
538
+ Creates a `@beignet/core/client` instance with Next.js-friendly base URL defaults.
539
+
540
+ ### `resolveNextBaseUrl(config?): string`
541
+
542
+ Resolves the base URL used by `createNextClient`.
543
+
544
+ ### `createOpenAPIHandler(contracts, options): (req: Request) => Promise<Response>`
545
+
546
+ Creates a Next.js route handler that returns an OpenAPI 3.1 JSON document. Requires `@beignet/core/openapi` in the app.
547
+
548
+ When you use central route registration, prefer `server.contracts` or
549
+ `contractsFromRoutes(routes)` so OpenAPI is generated from the same route list
550
+ used by the runtime.
551
+
552
+ ### `createSwaggerUIHandler(options?): (req: Request) => Response`
553
+
554
+ Creates a Next.js route handler that serves Swagger UI for an OpenAPI endpoint.
555
+
556
+ ### `toRequestLike(req: Request): HttpRequestLike`
557
+
558
+ Converts a Next.js `Request` to the framework-agnostic `HttpRequestLike` shape.
559
+
560
+ ### `toNextResponse(res: HttpResponseLike): Response`
561
+
562
+ Converts an `HttpResponseLike` to a Next.js `Response`.
563
+
564
+ These are used internally by the adapter but can be used directly if needed.
565
+
566
+ ## Examples
567
+
568
+ ### Basic CRUD API
569
+
570
+ ```typescript
571
+ // features/todos/contracts.ts
572
+ import { createContractGroup } from "@beignet/core/contracts";
573
+ import { z } from "zod";
574
+
575
+ const todos = createContractGroup()
576
+ .namespace("todos")
577
+ .prefix("/api/todos");
578
+
579
+ const todoSchema = z.object({
580
+ id: z.string(),
581
+ title: z.string(),
582
+ completed: z.boolean(),
583
+ });
584
+
585
+ export const listTodos = todos
586
+ .get("/")
587
+ .responses({ 200: z.array(todoSchema) });
588
+
589
+ export const getTodo = todos
590
+ .get("/:id")
591
+ .pathParams(z.object({ id: z.string() }))
592
+ .responses({ 200: todoSchema });
593
+
594
+ export const createTodo = todos
595
+ .post("/")
596
+ .body(z.object({ title: z.string() }))
597
+ .responses({ 201: todoSchema });
598
+
599
+ export const updateTodo = todos
600
+ .put("/:id")
601
+ .pathParams(z.object({ id: z.string() }))
602
+ .body(z.object({ title: z.string(), completed: z.boolean() }))
603
+ .responses({ 200: todoSchema });
604
+
605
+ export const deleteTodo = todos
606
+ .delete("/:id")
607
+ .pathParams(z.object({ id: z.string() }))
608
+ .responses({ 204: null });
609
+
610
+ // server/index.ts
611
+ import { createNextServer, defineRoutes } from "@beignet/next";
612
+ import * as todosContracts from "@/features/todos/contracts";
613
+
614
+ export const server = await createNextServer({
615
+ ports: {},
616
+ routes: defineRoutes([
617
+ { contract: todosContracts.listTodos, handle: async () => ({ status: 200, body: [] }) },
618
+ { contract: todosContracts.getTodo, handle: async ({ path }) => ({ status: 200, body: { id: path.id, title: "...", completed: false } }) },
619
+ { contract: todosContracts.createTodo, handle: async ({ body }) => ({ status: 201, body: { id: "1", ...body, completed: false } }) },
620
+ { contract: todosContracts.updateTodo, handle: async ({ path, body }) => ({ status: 200, body: { id: path.id, ...body } }) },
621
+ { contract: todosContracts.deleteTodo, handle: async () => ({ status: 204 }) },
622
+ ]),
623
+ createContext: async () => ({ todos: [] }),
624
+ mapUnhandledError: () => ({
625
+ status: 500,
626
+ body: {
627
+ code: "INTERNAL_SERVER_ERROR",
628
+ message: "Internal server error",
629
+ },
630
+ }),
631
+ });
632
+ ```
633
+
634
+ ```typescript
635
+ // app/api/[[...path]]/route.ts
636
+ import { server } from "@/server";
637
+
638
+ export const DELETE = server.api;
639
+ export const GET = server.api;
640
+ export const HEAD = server.api;
641
+ export const OPTIONS = server.api;
642
+ export const PATCH = server.api;
643
+ export const POST = server.api;
644
+ export const PUT = server.api;
645
+ ```
646
+
647
+ ### With authentication
648
+
649
+ ```typescript
650
+ // server/index.ts
651
+ import { createNextServer } from "@beignet/next";
652
+ import { AuthUnauthorizedError } from "@beignet/core/ports";
653
+ import { getTodo } from "@/features/todos/contracts";
654
+
655
+ export const server = await createNextServer({
656
+ ports: {},
657
+ createContext: async ({ req }) => {
658
+ const user = await getUserFromRequest(req);
659
+
660
+ if (!user) {
661
+ throw new AuthUnauthorizedError();
662
+ }
663
+
664
+ return { user };
665
+ },
666
+ mapUnhandledError: () => {
667
+ return {
668
+ status: 500,
669
+ body: {
670
+ code: "INTERNAL_SERVER_ERROR",
671
+ message: "Internal server error",
672
+ },
673
+ };
674
+ },
675
+ });
676
+ ```
677
+
678
+ ### Server component usage
679
+
680
+ You can call use cases directly from React Server Components using `createContextFromNext()`:
681
+
682
+ ```typescript
683
+ // app/todos/page.tsx
684
+ import { server } from "@/server";
685
+ import { listTodosUseCase } from "@/features/todos/use-cases/list-todos";
686
+
687
+ export default async function TodosPage() {
688
+ // Create context from Next.js runtime
689
+ const ctx = await server.createContextFromNext();
690
+
691
+ // Call use case directly - no API route needed!
692
+ const result = await listTodosUseCase.run({
693
+ ctx,
694
+ input: { limit: 10, offset: 0 }
695
+ });
696
+
697
+ return (
698
+ <div>
699
+ <h1>Todos</h1>
700
+ <ul>
701
+ {result.todos.map(todo => (
702
+ <li key={todo.id}>{todo.title}</li>
703
+ ))}
704
+ </ul>
705
+ </div>
706
+ );
707
+ }
708
+ ```
709
+
710
+ This approach:
711
+ - Eliminates unnecessary API routes for server-side data fetching
712
+ - Maintains type safety and business logic separation
713
+ - Automatically handles headers and cookies from Next.js
714
+ - Reuses your existing context creation logic
715
+
716
+ ## Related packages
717
+
718
+ - [@beignet/core/server](https://beignet.dev/server) - Framework-agnostic server runtime
719
+ - [@beignet/core/contracts](https://beignet.dev/contracts) - Contract definitions
720
+ - [@beignet/core/openapi](https://beignet.dev/openapi) - OpenAPI spec generation
721
+ - [@beignet/core/client](https://beignet.dev/client) - Type-safe HTTP client
722
+
723
+ ## License
724
+
725
+ MIT