next-zero-rpc 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,32 +38,46 @@ if (err) {
38
38
  }
39
39
  ```
40
40
 
41
- ### How it compares
42
-
43
- | Feature | next-zero-rpc | tRPC | raw fetch |
44
- | ------------------------- | -------------------- | ----------- | --------- |
45
- | Type-safe paths | | | |
46
- | Type-safe responses | | | |
47
- | Type-safe methods | ✅ | N/A | ❌ |
48
- | **Error type narrowing** | ✅ | ❌ | ❌ |
49
- | Zero runtime cost | ✅ (1.8 KB minified) | ❌ (~14 KB) | ✅ |
50
- | Zero config | | | |
51
- | Standard API routes | | ❌ | ✅ |
52
- | Dynamic params `[id]` | | | N/A |
53
- | Catch-all `[...slug]` | ✅ | ✅ | N/A |
54
- | Go-style error handling | | ❌ | |
55
- | Exhaustive error checking | | | |
56
- | Server action helpers | ✅ | ❌ | N/A |
57
- | Dependencies | 0 | 5+ | 0 |
41
+ ### The 2026 Ecosystem Comparison
42
+
43
+ | Feature | next-zero-rpc | tRPC | ts-rest | raw fetch |
44
+ | --------------------------- | -------------------------------------- | --------------------------------- | ------------------------------- | ----------------- |
45
+ | Primary Philosophy | Invisible type bridge | End-to-end framework | Contract-first API | Platform standard |
46
+ | Source of Truth | Next.js Route Handlers | tRPC Routers / Procedures | Shared contract.ts file | None |
47
+ | Type-safe paths & responses | ✅ | | ✅ | |
48
+ | Per-route error narrowing | ✅ (via TypeScript generics) | ❌ (Global error shapes) | ❌ (Standardized HTTP errors) | ❌ |
49
+ | Next.js App Router Native | ✅ (Zero changes to standard handlers) | ❌ (Requires tRPC adapters) | ❌ (Requires ts-rest adapters) | ✅ |
50
+ | Input Validation | Bring-your-own | Built-in (Zod heavily favored) | Built-in (Zod favored) | Bring-your-own |
51
+ | OpenAPI Generation | | ❌ (Requires third-party plugins) | ✅ (First-class citizen) | ❌ |
52
+ | Client Runtime Size | ~1.8 KB | ~15 KB | ~3-5 KB | 0 KB |
53
+ | Server Actions Integration | ✅ (Tuple-based Go-style returns) | ✅ (Excellent RSC/Action support) | Partial (Focus remains on REST) | N/A |
54
+ | Non-TS Client Support | | ❌ | ✅ (Via standard REST/OpenAPI) | ✅ |
55
+ | Ecosystem & Community | Niche / Lightweight | Massive / Enterprise-grade | Very Strong / Standardized | Ubiquitous |
56
+
57
+ #### Architectural Breakdown: Which to Choose?
58
+
59
+ ##### 1. next-zero-rpc (The Minimalist Bridge)
60
+
61
+ - **Best for:** Teams deeply invested in the Next.js App Router who want type safety without adopting a new framework paradigm.
62
+ - **The draw:** You write standard \`export async function GET(req)\` handlers. The library just quietly infers what you wrote. If you ever decide to remove the library, your backend code doesn't have to change at all.
63
+ - **The trade-off:** You give up the robust middleware pipelines, batched requests, and automatic OpenAPI generation that larger ecosystems provide.
64
+
65
+ ## When to use this
66
+
67
+ **Use `next-zero-rpc` when:**
68
+
69
+ - You're already writing plain Next.js App Router route handlers and want type-safe `fetch` calls _without restructuring your backend_ into tRPC procedures or ts-rest contracts
70
+ - Per-route error code narrowing matters to you — this is genuinely not available in tRPC or ts-rest out of the box
71
+ - You want a tiny client footprint (~1.8 KB) and zero ongoing npm dependencies
58
72
 
59
73
  ## Philosophy
60
74
 
61
75
  **You own the code, not the library.** `next-zero-rpc` is not a locked-in framework—it's a philosophy, a paradigm, and a set of methods for doing things. When you run `init`, we give you four files. From that moment on, they are _yours_ to modify, extend, or delete.
62
76
 
63
- - **Zero vendor lock-in** — There is no black-box `node_modules` dependency dictating your architecture. You own the fetch client, the error codes, and the registry generator.
77
+ - **Zero vendor lock-in** — There is no black-box `node_modules` dependency dictating your architecture. You own the fetch client, the error codes, and the registry generator. If the maintainer disappears tomorrow, you're not blocked.
64
78
  - **Zero boilerplate** — You write standard Next.js API route handlers using simple response helpers — no decorators, no schema registrations, no complex abstractions. The codegen reads what already exists and builds the type bridge automatically.
65
79
  - **Not a framework** — It's a type bridge. It infers what your route handlers return and gives your client code full type safety over those responses.
66
- - **Validation is yours** — Input validation (Zod, Valibot, Arktype, manual checks) stays inside your route handler where it belongs. This library doesn't impose a validation layer — that's a feature, not a gap.
80
+ - **Validation is yours** — Input validation (Zod, Valibot, Arktype, manual checks) stays inside your route handler where it belongs. This library doesn't impose a validation layer — that's a deliberate design choice.
67
81
  - **Non-invasive** — Unlike tRPC or ts-rest, you don't adopt a new API definition pattern. Your routes are regular Next.js routes. The library is invisible.
68
82
 
69
83
  ## Setup
@@ -177,7 +191,7 @@ This works because:
177
191
 
178
192
  1. `createApiError` is generic: `createApiError<C extends ErrorCode>(code: C, ...) → NextResponse<ApiErrorPayload<C>>`
179
193
  2. TypeScript infers the literal `C` from each call site in your handler
180
- 3. `InferErrorApiResponse` extracts the union of all `ApiErrorPayload<C>` types from the handler's return type
194
+ 3. `UnwrapNextResponse` extracts the union of all `ApiErrorPayload<C>` types from the handler's return type
181
195
  4. The client sees only those specific error codes
182
196
 
183
197
  ### Go-Style Tuple Returns
@@ -241,6 +255,18 @@ const [data, err] = await apiFetch("/api/extreme/org1/projects/proj1/tasks/a/b/c
241
255
  const [data, err] = await apiFetch("/api/users/123?include=profile", { method: "GET" });
242
256
  ```
243
257
 
258
+ ### Static vs Dynamic Route Precedence
259
+
260
+ If you have overlapping static and dynamic routes (e.g., `/api/users/active` and `/api/users/[userId]`), `next-zero-rpc` correctly gives **exact static matches precedence** over dynamic segments at compile time.
261
+
262
+ ```typescript
263
+ // Safely infers the type of the `active` route, completely ignoring the `[userId]` route
264
+ const [activeUsers] = await apiFetch("/api/users/active", { method: "GET" });
265
+
266
+ // Safely infers the type of the `[userId]` route
267
+ const [singleUser] = await apiFetch("/api/users/123", { method: "GET" });
268
+ ```
269
+
244
270
  ### Route Groups Support
245
271
 
246
272
  Next.js route groups like `(groupName)` are natively supported. They are automatically ignored in the URL path mapping and the generated TypeScript types, perfectly matching Next.js behavior:
@@ -369,15 +395,15 @@ HTTP_STATUS_ERROR.GATEWAY_TIMEOUT; // 504
369
395
 
370
396
  #### Functions
371
397
 
372
- | Function | Signature | Description |
373
- | ------------------------- | ------------------------------------------------------------------------------ | ------------------------------------- |
374
- | `createApiError<C>` | `(code: C, statusCode, options?) → NextResponse<ApiErrorPayload<C>>` | Create a typed API error response |
375
- | `createApiSuccess<T>` | `(data: T, statusCode?) → NextResponse<T>` | Create a typed API success response |
376
- | `createApiSuccess` | `(undefined, NO_CONTENT) → NextResponse<undefined>` | Overload for 204 No Content |
377
- | `isApiErrorPayload` | `(payload: unknown) → payload is ApiErrorPayload<ErrorCode>` | Runtime type guard for error payloads |
378
- | `createServiceError` | `(code, options?) → [null, ServiceError]` | Go-style error for server actions |
379
- | `createServiceSuccess<T>` | `(data?: T) → [T \| undefined, null]` | Go-style success for server actions |
380
- | `assertNever` | `(value: never) → never` | Compile-time exhaustiveness guard |
398
+ | Function | Signature | Description |
399
+ | ------------------------- | -------------------------------------------------------------------- | ------------------------------------- |
400
+ | `createApiError<C>` | `(code: C, statusCode, options?) → NextResponse<ApiErrorPayload<C>>` | Create a typed API error response |
401
+ | `createApiSuccess<T>` | `(data: T, statusCode?) → NextResponse<T>` | Create a typed API success response |
402
+ | `createApiSuccess` | `(undefined, NO_CONTENT) → NextResponse<undefined>` | Overload for 204 No Content |
403
+ | `isApiErrorPayload` | `(payload: unknown) → payload is ApiErrorPayload<ErrorCode>` | Runtime type guard for error payloads |
404
+ | `createServiceError` | `(code, options?) → [null, ServiceError]` | Go-style error for server actions |
405
+ | `createServiceSuccess<T>` | `(data?: T) → [T \| undefined, null]` | Go-style success for server actions |
406
+ | `assertNever` | `(value: never) → never` | Compile-time exhaustiveness guard |
381
407
 
382
408
  ### `apiClient.ts`
383
409
 
@@ -515,7 +541,7 @@ Since you own all the source files, you can customize anything:
515
541
 
516
542
  [![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/p/sandbox/github/caocchinh/next-zero-rpc/tree/main/examples/minimal)
517
543
 
518
- Opens [`examples/minimal`](https://github.com/caocchinh/next-zero-rpc/tree/main/examples/minimal) — a self-contained Next.js app with a working `apiFetch` client and two example API routes.
544
+ Opens [`examples/minimal`](./examples/minimal) — a self-contained Next.js app with a working `apiFetch` client and two example API routes.
519
545
 
520
546
  ### Run locally
521
547
 
@@ -538,11 +564,11 @@ npx next-zero-rpc --help # Show help
538
564
 
539
565
  ## Requirements
540
566
 
541
- | Requirement | Minimum Version | Reason |
542
- | ----------- | --------------- | ------ |
543
- | Next.js | **14.0** | App Router (stable since 14.0) |
544
- | TypeScript | **4.9** | `satisfies` keyword used in `responses.ts` |
545
- | Node.js | **18** | Native `fetch` API required by `apiFetch` |
567
+ | Requirement | Minimum Version | Reason |
568
+ | ----------- | --------------- | ------------------------------------------ |
569
+ | Next.js | **14.0** | App Router (stable since 14.0) |
570
+ | TypeScript | **4.9** | `satisfies` keyword used in `responses.ts` |
571
+ | Node.js | **18** | Native `fetch` API required by `apiFetch` |
546
572
 
547
573
  ## License
548
574
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-zero-rpc",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Type-safe fetch for Next.js — zero runtime, zero config, zero dependencies.",
5
5
  "keywords": [
6
6
  "nextjs",
@@ -18,8 +18,8 @@ type UnwrapNextResponse<T> = T extends (...args: never[]) => infer R
18
18
  type ResolveRoute<Path extends string> =
19
19
  FindMatchingRoute<Path> extends keyof KnownRoutes ? FindMatchingRoute<Path> : never;
20
20
 
21
- type RouteHandler<Path extends string, M extends HttpMethod> =
22
- KnownRoutes[ResolveRoute<Path>][M & keyof KnownRoutes[ResolveRoute<Path>]];
21
+ type RouteHandler<Path extends string, M extends HttpMethod> = KnownRoutes[ResolveRoute<Path>][M &
22
+ keyof KnownRoutes[ResolveRoute<Path>]];
23
23
 
24
24
  type RouteMethods<Path extends string> = Extract<keyof KnownRoutes[ResolveRoute<Path>], HttpMethod>;
25
25
 
@@ -56,7 +56,7 @@ export async function apiFetch(
56
56
 
57
57
  let payload;
58
58
  // An empty HTTP body resolves to an empty string "" (falsy).
59
- // Valid JSON primitives like `0`, `null`, `false`, or `""` serialize to
59
+ // Valid JSON primitives like `0`, `null`, `false`, or `""` serialize to
60
60
  // length > 0 strings (e.g. `"0"`, `"null"`, `'""'`), which are all truthy.
61
61
  // This perfectly catches empty responses (like 204) while preserving valid JSON.
62
62
  if (!text) {
@@ -37,11 +37,16 @@ type MatchSegments<P extends string[], K extends string[]> = K extends []
37
37
 
38
38
  type StripQuery<Path extends string> = Path extends `${infer Base}?${string}` ? Base : Path;
39
39
 
40
- export type FindMatchingRoute<Path extends string> = {
41
- [K in keyof KnownRoutes & string]: MatchSegments<Split<StripQuery<Path>>, Split<K>> extends true
42
- ? K
43
- : never;
44
- }[keyof KnownRoutes & string];
40
+ export type FindMatchingRoute<Path extends string> = Path extends keyof KnownRoutes
41
+ ? Path
42
+ : {
43
+ [K in keyof KnownRoutes & string]: MatchSegments<
44
+ Split<StripQuery<Path>>,
45
+ Split<K>
46
+ > extends true
47
+ ? K
48
+ : never;
49
+ }[keyof KnownRoutes & string];
45
50
 
46
51
  export type CheckPath<Path extends string> = Path extends ""
47
52
  ? keyof KnownRoutes