ertk 0.1.0

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,717 @@
1
+ # ERTK — Easy RTK
2
+
3
+ Define endpoints once, generate RTK Query hooks and Next.js route handlers automatically.
4
+
5
+ ERTK is a TypeScript code generation tool that eliminates the boilerplate of writing RTK Query APIs and Next.js App Router route handlers. You define your endpoints in simple, type-safe files — ERTK generates the rest.
6
+
7
+ ## Features
8
+
9
+ - **Single source of truth** — Define each endpoint once with its name, method, validation, tags, and handler
10
+ - **RTK Query codegen** — Generates a fully typed `api.ts` with `createApi`, hooks, and cache tag configuration
11
+ - **Redux store scaffolding** — Generates a ready-to-use `store.ts` with the API middleware wired up
12
+ - **Next.js App Router routes** — Generates `route.ts` files that map HTTP methods to your handlers
13
+ - **Cache invalidation helpers** — Generates `invalidation.ts` with re-exported utilities
14
+ - **Optimistic updates** — Declarative single and multi-target optimistic update configuration
15
+ - **Validation** — Works with Zod (v3 & v4), Valibot, ArkType, or any schema with a `.parse()` method
16
+ - **Auth adapters** — Pluggable authentication via a simple `getUser(req)` interface
17
+ - **Incremental builds** — Manifest-based change detection skips generation when nothing changed
18
+ - **Watch mode** — Watches endpoint files and regenerates on save with 300ms debouncing
19
+ - **Path alias detection** — Auto-reads `tsconfig.json` paths to generate correct import paths
20
+ - **Custom error handlers** — Chainable error handlers for ORM-specific or domain errors
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install ertk
26
+ # or
27
+ pnpm add ertk
28
+ # or
29
+ yarn add ertk
30
+ ```
31
+
32
+ ### Peer Dependencies
33
+
34
+ ERTK requires the following peer dependencies:
35
+
36
+ | Package | Version | Required |
37
+ |---------|---------|----------|
38
+ | `@reduxjs/toolkit` | `^2.0.0` | Yes |
39
+ | `react` | `>=18.0.0` | Yes |
40
+ | `react-redux` | `^9.0.0` | Yes |
41
+ | `typescript` | `^5.0.0` | Yes |
42
+ | `next` | `>=14.0.0` | Only for route generation |
43
+ | `zod` | `^3.0.0 \|\| ^4.0.0` | Only if using Zod validation |
44
+
45
+ ## Quick Start
46
+
47
+ ### 1. Initialize your project
48
+
49
+ ```bash
50
+ npx ertk init
51
+ ```
52
+
53
+ This creates:
54
+ - `ertk.config.ts` — Configuration file
55
+ - `src/endpoints/` — Directory for endpoint definitions
56
+ - `src/generated/` — Directory for generated output
57
+
58
+ ### 2. Define an endpoint
59
+
60
+ ```typescript
61
+ // src/endpoints/tasks/list.ts
62
+ import { endpoint } from "ertk";
63
+ import type { Task } from "@app/types/task";
64
+
65
+ export default endpoint.get<Task[]>({
66
+ name: "listTasks",
67
+ protected: true,
68
+ query: () => "/tasks",
69
+ tags: {
70
+ provides: ["Tasks"],
71
+ },
72
+ handler: async ({ user }) => {
73
+ return await db.task.findMany({ where: { userId: user.id } });
74
+ },
75
+ });
76
+ ```
77
+
78
+ ### 3. Generate
79
+
80
+ ```bash
81
+ npx ertk generate
82
+ ```
83
+
84
+ ### 4. Use the generated hooks
85
+
86
+ ```tsx
87
+ import { useListTasksQuery } from "@app/generated/api";
88
+
89
+ function TaskList() {
90
+ const { data: tasks, isLoading } = useListTasksQuery();
91
+
92
+ if (isLoading) return <p>Loading...</p>;
93
+
94
+ return (
95
+ <ul>
96
+ {tasks?.map((task) => (
97
+ <li key={task.id}>{task.title}</li>
98
+ ))}
99
+ </ul>
100
+ );
101
+ }
102
+ ```
103
+
104
+ ## CLI
105
+
106
+ ```
107
+ ertk — Easy RTK Query codegen
108
+
109
+ Usage:
110
+ ertk generate One-shot generation (skips if nothing changed)
111
+ ertk generate --watch Watch mode with incremental regeneration
112
+ ertk init Scaffold config file and directories
113
+ ertk --help Show this help message
114
+
115
+ Options:
116
+ --watch Watch for endpoint file changes and regenerate
117
+ --help Show help
118
+ ```
119
+
120
+ ### `ertk init`
121
+
122
+ Scaffolds the project structure. Creates `ertk.config.ts` and the `src/endpoints/` and `src/generated/` directories if they don't exist.
123
+
124
+ ### `ertk generate`
125
+
126
+ Runs a one-shot generation. Compares an MD5 manifest of all endpoint files against the previous run and skips generation if nothing has changed.
127
+
128
+ ### `ertk generate --watch`
129
+
130
+ Runs an initial full build, then watches the endpoints directory for file changes. Uses a 300ms debounce to batch rapid saves. When an endpoint file is modified, only that file is re-parsed and the full output is regenerated.
131
+
132
+ ## Configuration
133
+
134
+ Create an `ertk.config.ts` (or `.mts`, `.js`, `.mjs`) in your project root:
135
+
136
+ ```typescript
137
+ import { defineConfig } from "ertk";
138
+
139
+ export default defineConfig({
140
+ // Directory containing endpoint definition files
141
+ endpoints: "src/endpoints",
142
+
143
+ // Directory for generated output (api.ts, store.ts, invalidation.ts)
144
+ generated: "src/generated",
145
+
146
+ // Base URL for RTK Query fetchBaseQuery
147
+ baseUrl: "/api",
148
+
149
+ // Route generation config (omit entirely to skip route generation)
150
+ routes: {
151
+ dir: "src/app/api",
152
+ handlerModule: "ertk/next",
153
+ ignoredRoutes: ["auth"],
154
+ },
155
+ });
156
+ ```
157
+
158
+ ### Config Options
159
+
160
+ | Option | Type | Default | Description |
161
+ |--------|------|---------|-------------|
162
+ | `endpoints` | `string` | `"src/endpoints"` | Directory containing endpoint definition files |
163
+ | `generated` | `string` | `"src/generated"` | Directory for generated output files |
164
+ | `baseUrl` | `string` | `"/api"` | Base URL for `fetchBaseQuery` |
165
+ | `baseQuery` | `string` | — | Custom `baseQuery` source code (overrides `baseUrl`) |
166
+ | `pathAlias` | `string` | auto-detected | Path alias prefix (e.g., `"@app"`, `"@src"`) |
167
+ | `crudFilenames` | `string[]` | see below | Filenames that map to CRUD operations |
168
+ | `routes` | `object \| undefined` | — | Route generation config; omit to skip |
169
+
170
+ **Default CRUD filenames:** `["get", "list", "create", "update", "delete", "send", "remove", "cancel"]`
171
+
172
+ CRUD filenames determine which endpoint filenames become URL segments and which don't. For example, `src/endpoints/tasks/list.ts` generates the route `/api/tasks` (not `/api/tasks/list`) because `list` is a CRUD filename.
173
+
174
+ ### Route Generation Options
175
+
176
+ | Option | Type | Default | Description |
177
+ |--------|------|---------|-------------|
178
+ | `routes.dir` | `string` | — | Directory where Next.js route files are generated |
179
+ | `routes.handlerModule` | `string` | `"ertk/next"` | Module that exports `createRouteHandler` |
180
+ | `routes.ignoredRoutes` | `string[]` | `[]` | Top-level route directories to skip |
181
+
182
+ ### Custom `baseQuery`
183
+
184
+ For full control over fetch configuration (auth headers, base URLs, etc.):
185
+
186
+ ```typescript
187
+ export default defineConfig({
188
+ baseQuery: `fetchBaseQuery({
189
+ baseUrl: "https://api.example.com",
190
+ prepareHeaders: (headers) => {
191
+ headers.set("Authorization", \`Bearer \${getToken()}\`);
192
+ return headers;
193
+ },
194
+ })`,
195
+ });
196
+ ```
197
+
198
+ ### Path Alias Auto-Detection
199
+
200
+ ERTK automatically reads your `tsconfig.json` to detect path aliases. If you have:
201
+
202
+ ```json
203
+ {
204
+ "compilerOptions": {
205
+ "paths": {
206
+ "@app/*": ["./src/*"]
207
+ }
208
+ }
209
+ }
210
+ ```
211
+
212
+ ERTK will use `@app` as the import prefix in generated files. If no alias is found, it defaults to `@app` with a `src` root.
213
+
214
+ ## Endpoint Definitions
215
+
216
+ Endpoints are defined using the `endpoint` factory, which provides methods for each HTTP verb:
217
+
218
+ ```typescript
219
+ import { endpoint } from "ertk";
220
+
221
+ // Available methods
222
+ endpoint.get<ResponseType, ArgsType>({ ... })
223
+ endpoint.post<ResponseType, ArgsType>({ ... })
224
+ endpoint.put<ResponseType, ArgsType>({ ... })
225
+ endpoint.patch<ResponseType, ArgsType>({ ... })
226
+ endpoint.delete<ResponseType, ArgsType>({ ... })
227
+ ```
228
+
229
+ Each file should have a single `default export` of an endpoint definition.
230
+
231
+ ### Endpoint Options
232
+
233
+ | Option | Type | Default | Description |
234
+ |--------|------|---------|-------------|
235
+ | `name` | `string` | — | **Required.** Name for the generated hook (e.g., `"getTasks"` becomes `useGetTasksQuery`) |
236
+ | `protected` | `boolean` | `true` | Whether the endpoint requires authentication |
237
+ | `query` | `(args) => string \| { url, method?, body? }` | — | Client-side query function for RTK Query |
238
+ | `request` | `ValidationSchema` | — | Request validation schema (Zod, Valibot, etc.) |
239
+ | `tags` | `{ provides?, invalidates? }` | — | RTK Query cache tag configuration |
240
+ | `optimistic` | `SingleOptimistic \| MultiOptimistic` | — | Optimistic update configuration |
241
+ | `handler` | `(ctx) => Promise<unknown>` | — | Server-side handler (omit for client-only endpoints) |
242
+
243
+ ### GET Endpoint (Query)
244
+
245
+ ```typescript
246
+ // src/endpoints/tasks/get.ts
247
+ import { endpoint } from "ertk";
248
+ import type { Task } from "@app/types/task";
249
+
250
+ export default endpoint.get<Task, { id: string }>({
251
+ name: "getTask",
252
+ protected: true,
253
+ query: ({ id }) => `/tasks/${id}`,
254
+ tags: {
255
+ provides: (result, _error, { id }) => [{ type: "Tasks", id }],
256
+ },
257
+ handler: async ({ query, user }) => {
258
+ return await db.task.findUnique({
259
+ where: { id: query.id, userId: user.id },
260
+ });
261
+ },
262
+ });
263
+ ```
264
+
265
+ ### POST Endpoint (Mutation)
266
+
267
+ ```typescript
268
+ // src/endpoints/tasks/create.ts
269
+ import { endpoint } from "ertk";
270
+ import { z } from "zod";
271
+ import type { Task, CreateTaskInput } from "@app/types/task";
272
+
273
+ const createTaskSchema = z.object({
274
+ title: z.string().min(1),
275
+ description: z.string().optional(),
276
+ });
277
+
278
+ export default endpoint.post<Task, CreateTaskInput>({
279
+ name: "createTask",
280
+ protected: true,
281
+ request: createTaskSchema,
282
+ query: (body) => ({ url: "/tasks", method: "POST", body }),
283
+ tags: {
284
+ invalidates: ["Tasks"],
285
+ },
286
+ handler: async ({ body, user }) => {
287
+ return await db.task.create({
288
+ data: { ...body, userId: user.id },
289
+ });
290
+ },
291
+ });
292
+ ```
293
+
294
+ ### Client-Only Endpoint (No Handler)
295
+
296
+ For endpoints that consume an external API (no server-side handler needed):
297
+
298
+ ```typescript
299
+ // src/endpoints/weather/get.ts
300
+ import { endpoint } from "ertk";
301
+ import type { WeatherData } from "@app/types/weather";
302
+
303
+ export default endpoint.get<WeatherData, { city: string }>({
304
+ name: "getWeather",
305
+ protected: false,
306
+ query: ({ city }) => `/weather?city=${city}`,
307
+ });
308
+ ```
309
+
310
+ Client-only endpoints (no `handler`) are excluded from route generation but are still included in the generated RTK Query API.
311
+
312
+ ### File Structure and Route Mapping
313
+
314
+ Endpoint file paths map to API routes. CRUD filenames (configurable) are stripped from the URL:
315
+
316
+ | File Path | Route |
317
+ |-----------|-------|
318
+ | `src/endpoints/tasks/list.ts` | `/api/tasks` |
319
+ | `src/endpoints/tasks/create.ts` | `/api/tasks` |
320
+ | `src/endpoints/tasks/get.ts` | `/api/tasks` |
321
+ | `src/endpoints/users/profile/update.ts` | `/api/users/profile` |
322
+ | `src/endpoints/billing/invoices.ts` | `/api/billing/invoices` |
323
+
324
+ Multiple endpoints that resolve to the same route are grouped into a single `route.ts` file, each exported as the appropriate HTTP method (`GET`, `POST`, `PUT`, etc.).
325
+
326
+ ## Generated Output
327
+
328
+ Running `ertk generate` produces the following files:
329
+
330
+ ### `api.ts`
331
+
332
+ The RTK Query API definition with all endpoints and exported hooks:
333
+
334
+ ```typescript
335
+ // AUTO-GENERATED by ERTK codegen. Do not edit.
336
+ import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
337
+ import type { Task } from "@app/types/task";
338
+
339
+ export const api = createApi({
340
+ reducerPath: "api",
341
+ baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
342
+ tagTypes: ["Tasks"],
343
+ refetchOnFocus: false,
344
+ refetchOnReconnect: true,
345
+ endpoints: (builder) => ({
346
+ listTasks: builder.query<Task[], void>({
347
+ query: () => "/tasks",
348
+ providesTags: ["Tasks"],
349
+ }),
350
+ createTask: builder.mutation<Task, CreateTaskInput>({
351
+ query: (body) => ({ url: "/tasks", method: "POST", body }),
352
+ invalidatesTags: ["Tasks"],
353
+ }),
354
+ }),
355
+ });
356
+
357
+ export const {
358
+ useListTasksQuery,
359
+ useCreateTaskMutation,
360
+ } = api;
361
+ ```
362
+
363
+ ### `store.ts`
364
+
365
+ A pre-configured Redux store:
366
+
367
+ ```typescript
368
+ // AUTO-GENERATED by ERTK codegen. Do not edit.
369
+ import { configureStore } from "@reduxjs/toolkit";
370
+ import { api } from "./api";
371
+
372
+ export const store = configureStore({
373
+ reducer: {
374
+ [api.reducerPath]: api.reducer,
375
+ },
376
+ middleware: (getDefaultMiddleware) =>
377
+ getDefaultMiddleware().concat(api.middleware),
378
+ });
379
+
380
+ export type RootState = ReturnType<typeof store.getState>;
381
+ export type AppDispatch = typeof store.dispatch;
382
+ ```
383
+
384
+ ### `invalidation.ts`
385
+
386
+ Cache invalidation helper re-exports:
387
+
388
+ ```typescript
389
+ // AUTO-GENERATED by ERTK codegen. Do not edit.
390
+ import { api } from "./api";
391
+
392
+ export function invalidateTags(
393
+ ...args: Parameters<typeof api.util.invalidateTags>
394
+ ) {
395
+ return api.util.invalidateTags(...args);
396
+ }
397
+
398
+ export const updateQueryData = api.util.updateQueryData;
399
+ ```
400
+
401
+ ### Route Files (Next.js)
402
+
403
+ Generated in your configured routes directory (e.g., `src/app/api/tasks/route.ts`):
404
+
405
+ ```typescript
406
+ // AUTO-GENERATED by ERTK codegen. Do not edit.
407
+ import { createRouteHandler } from "ertk/next";
408
+ import listTasksEndpoint from "@app/endpoints/tasks/list";
409
+ import createTaskEndpoint from "@app/endpoints/tasks/create";
410
+
411
+ export const GET = createRouteHandler(listTasksEndpoint);
412
+ export const POST = createRouteHandler(createTaskEndpoint);
413
+ ```
414
+
415
+ ## Next.js Route Handlers
416
+
417
+ ### Setting Up Auth
418
+
419
+ For protected endpoints, configure an auth adapter:
420
+
421
+ ```typescript
422
+ // src/lib/ertk-handler.ts
423
+ import { configureHandler } from "ertk/next";
424
+ import { getServerSession } from "next-auth";
425
+ import { authOptions } from "@app/lib/auth";
426
+ import { db } from "@app/lib/db";
427
+
428
+ export const createRouteHandler = configureHandler({
429
+ auth: {
430
+ getUser: async (req) => {
431
+ const session = await getServerSession(authOptions);
432
+ if (!session?.user?.email) return null;
433
+ return await db.user.findUnique({
434
+ where: { email: session.user.email },
435
+ });
436
+ },
437
+ },
438
+ });
439
+ ```
440
+
441
+ Then set `handlerModule` in your config to point to your custom module:
442
+
443
+ ```typescript
444
+ // ertk.config.ts
445
+ export default defineConfig({
446
+ routes: {
447
+ dir: "src/app/api",
448
+ handlerModule: "@app/lib/ertk-handler",
449
+ },
450
+ });
451
+ ```
452
+
453
+ ### Custom Error Handlers
454
+
455
+ Add ORM-specific or domain-specific error handling:
456
+
457
+ ```typescript
458
+ import { configureHandler } from "ertk/next";
459
+ import { Prisma } from "@prisma/client";
460
+
461
+ export const createRouteHandler = configureHandler({
462
+ auth: { /* ... */ },
463
+ errorHandlers: [
464
+ (error) => {
465
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
466
+ if (error.code === "P2025") {
467
+ return new Response(
468
+ JSON.stringify({ error: "Not found" }),
469
+ { status: 404, headers: { "Content-Type": "application/json" } },
470
+ );
471
+ }
472
+ }
473
+ return null; // Pass to next handler
474
+ },
475
+ ],
476
+ });
477
+ ```
478
+
479
+ Error handlers are processed in order. The first handler to return a non-null `Response` wins. If no handler matches, ERTK falls back to built-in handling:
480
+
481
+ 1. `ValidationError` → 400 with validation details
482
+ 2. Errors with a numeric `status` property → uses that status code
483
+ 3. All other errors → 500 with generic message (details logged server-side)
484
+
485
+ ### Request Parsing
486
+
487
+ ERTK automatically handles request parsing based on the HTTP method:
488
+
489
+ - **GET, DELETE, HEAD, OPTIONS** — Parses `URLSearchParams` into an object (with automatic string-to-number coercion)
490
+ - **POST, PUT, PATCH** — Parses JSON request body
491
+
492
+ If a `request` schema is provided on the endpoint, the parsed data is validated through `schema.parse()` before reaching the handler.
493
+
494
+ ### Handler Context
495
+
496
+ Every handler receives a context object:
497
+
498
+ ```typescript
499
+ interface HandlerContext<TBody, TQuery, TUser> {
500
+ user: TUser; // Resolved user (from auth adapter)
501
+ body: TBody; // Parsed & validated request body
502
+ query: TQuery; // Parsed & validated query parameters
503
+ params: Record<string, string>; // URL path parameters (Next.js dynamic segments)
504
+ req: Request; // Raw Request object
505
+ }
506
+ ```
507
+
508
+ ## Cache Tags
509
+
510
+ ERTK supports RTK Query's full tag system for automatic cache invalidation.
511
+
512
+ ### Static Tags
513
+
514
+ ```typescript
515
+ export default endpoint.get<Task[]>({
516
+ name: "listTasks",
517
+ tags: {
518
+ provides: ["Tasks"],
519
+ },
520
+ // ...
521
+ });
522
+
523
+ export default endpoint.post<Task, CreateTaskInput>({
524
+ name: "createTask",
525
+ tags: {
526
+ invalidates: ["Tasks"],
527
+ },
528
+ // ...
529
+ });
530
+ ```
531
+
532
+ ### Dynamic Tags
533
+
534
+ ```typescript
535
+ export default endpoint.get<Task, { id: string }>({
536
+ name: "getTask",
537
+ tags: {
538
+ provides: (result, _error, { id }) => [{ type: "Tasks", id }],
539
+ },
540
+ // ...
541
+ });
542
+
543
+ export default endpoint.put<Task, { id: string; title: string }>({
544
+ name: "updateTask",
545
+ tags: {
546
+ invalidates: (_result, _error, { id }) => [
547
+ { type: "Tasks", id },
548
+ "Tasks",
549
+ ],
550
+ },
551
+ // ...
552
+ });
553
+ ```
554
+
555
+ Tag types are automatically extracted from your endpoint definitions and included in the generated `createApi({ tagTypes: [...] })` call.
556
+
557
+ ## Optimistic Updates
558
+
559
+ ERTK supports declarative optimistic updates that generate the `onQueryStarted` boilerplate for you.
560
+
561
+ ### Single Target
562
+
563
+ Update a single cached query when a mutation fires:
564
+
565
+ ```typescript
566
+ export default endpoint.put<Task, { id: string; completed: boolean }>({
567
+ name: "toggleTask",
568
+ optimistic: {
569
+ target: "listTasks",
570
+ args: (params) => undefined,
571
+ update: (draft, params) => {
572
+ const tasks = draft as Task[];
573
+ const task = tasks.find((t) => t.id === params.id);
574
+ if (task) task.completed = params.completed;
575
+ },
576
+ },
577
+ // ...
578
+ });
579
+ ```
580
+
581
+ ### Multi Target
582
+
583
+ Update multiple cached queries with optional conditions:
584
+
585
+ ```typescript
586
+ export default endpoint.delete<void, { id: string; listId: string }>({
587
+ name: "deleteTask",
588
+ optimistic: {
589
+ updates: [
590
+ {
591
+ target: "listTasks",
592
+ args: (params) => undefined,
593
+ update: (draft, params) => {
594
+ const tasks = draft as Task[];
595
+ const index = tasks.findIndex((t) => t.id === params.id);
596
+ if (index !== -1) tasks.splice(index, 1);
597
+ },
598
+ },
599
+ {
600
+ target: "getTaskList",
601
+ args: (params) => params.listId,
602
+ update: (draft, params) => {
603
+ const list = draft as TaskList;
604
+ list.count -= 1;
605
+ },
606
+ condition: (params) => !!params.listId,
607
+ },
608
+ ],
609
+ },
610
+ // ...
611
+ });
612
+ ```
613
+
614
+ The generated code automatically handles `queryFulfilled` awaiting and rolls back all patches on failure.
615
+
616
+ ## Validation
617
+
618
+ ERTK works with any validation library that exposes a `.parse(data) => T` method.
619
+
620
+ ### With Zod
621
+
622
+ ```typescript
623
+ import { z } from "zod";
624
+
625
+ const createTaskSchema = z.object({
626
+ title: z.string().min(1),
627
+ description: z.string().optional(),
628
+ priority: z.enum(["low", "medium", "high"]).default("medium"),
629
+ });
630
+
631
+ export default endpoint.post<Task, z.infer<typeof createTaskSchema>>({
632
+ name: "createTask",
633
+ request: createTaskSchema,
634
+ // ...
635
+ });
636
+ ```
637
+
638
+ ### With Any `.parse()` Compatible Library
639
+
640
+ ```typescript
641
+ const schema = {
642
+ parse: (data: unknown) => {
643
+ // Custom validation logic
644
+ if (!data || typeof data !== "object") throw new Error("Invalid input");
645
+ return data as MyType;
646
+ },
647
+ };
648
+
649
+ export default endpoint.post<MyType, MyInput>({
650
+ name: "createItem",
651
+ request: schema,
652
+ // ...
653
+ });
654
+ ```
655
+
656
+ Validation errors are caught by the route handler and returned as 400 responses with structured error details when using Zod.
657
+
658
+ ## API Reference
659
+
660
+ ### `ertk` (Main Entry Point)
661
+
662
+ | Export | Type | Description |
663
+ |--------|------|-------------|
664
+ | `endpoint` | `object` | Factory with `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()` methods |
665
+ | `defineConfig` | `(config: ErtkConfig) => ErtkConfig` | Type-safe config wrapper |
666
+
667
+ ### `ertk/next` (Next.js Entry Point)
668
+
669
+ | Export | Type | Description |
670
+ |--------|------|-------------|
671
+ | `configureHandler` | `(options?) => createRouteHandler` | Creates a configured route handler factory |
672
+ | `createRouteHandler` | `(def) => RequestHandler` | Default handler (no auth, no custom errors) |
673
+ | `ErtkAuthAdapter` | `interface` | Auth adapter shape: `{ getUser(req) => Promise<User \| null> }` |
674
+ | `ErtkErrorHandler` | `type` | Error handler: `(error) => Response \| null` |
675
+ | `ConfigureHandlerOptions` | `interface` | Options for `configureHandler` |
676
+
677
+ ### Types
678
+
679
+ | Type | Description |
680
+ |------|-------------|
681
+ | `EndpointDefinition<TResponse, TArgs>` | Main endpoint configuration interface |
682
+ | `HandlerContext<TBody, TQuery, TUser>` | Server-side handler context |
683
+ | `DefaultUser` | Minimal user shape (`{ id: string }`) |
684
+ | `ValidationSchema<T>` | Generic validation interface (`.parse()` compatible) |
685
+ | `TagType` | String tag identifier |
686
+ | `TagDescription` | Tag string or `{ type, id }` object |
687
+ | `SingleOptimistic<TArgs>` | Single-target optimistic update config |
688
+ | `MultiOptimistic<TArgs>` | Multi-target optimistic update config |
689
+ | `ErtkConfig` | User-facing config type |
690
+ | `ErtkRoutesConfig` | Route generation config type |
691
+
692
+ ## Known Issues and Caveats
693
+
694
+ ### Endpoint Parsing
695
+
696
+ - **Malformed endpoints are silently skipped.** If an endpoint file lacks a default export, an `endpoint.{method}()` call, or a `name` property, it is skipped with a `console.warn`. Check your terminal output if endpoints are missing from generated code.
697
+ - **AST extraction assumes standard patterns.** The parser expects `endpoint.get<...>({ ... })` call syntax directly. Wrapping in helper functions, using spread operators, or storing the config in a separate variable may not be detected.
698
+ - **Type imports are not transitively resolved.** Only types directly imported in the endpoint file are carried over to the generated `api.ts`. If your response type re-exports from another module, you may need to import the underlying type directly.
699
+
700
+ ### Optimistic Updates
701
+
702
+ - **Parsed via regex, not AST.** The optimistic update extraction uses regex matching, which can break with unusual formatting, computed property names, or complex expressions inside `target`, `args`, or `update` fields. Keep optimistic configurations simple and well-formatted.
703
+
704
+ ### Route Generation
705
+
706
+ - **Deleted endpoints don't clean up routes.** In watch mode, if you delete an endpoint file, the corresponding route handler file is not automatically removed. You'll need to delete stale route files manually or re-run a fresh `ertk generate` after cleaning the output directory.
707
+ - **Route path validation is minimal.** Generated route paths are derived from file paths without checking for special characters that could produce invalid Next.js route segments.
708
+
709
+ ### General
710
+
711
+ - **`refetchOnFocus` and `refetchOnReconnect` are hardcoded.** The generated API sets `refetchOnFocus: false` and `refetchOnReconnect: true`. These are not yet configurable via `ertk.config.ts`.
712
+ - **No formatting of generated code.** Generated files use tabs and don't pass through Prettier or ESLint. Add generated paths to your formatter's include list if you want consistent style.
713
+ - **No test suite.** The package does not currently include automated tests.
714
+
715
+ ## License
716
+
717
+ MIT