next-zero-rpc 0.1.5 → 0.1.7
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 +80 -30
- package/package.json +1 -1
- package/templates/apiClient.ts +3 -3
- package/templates/apiRegistry.ts +7 -5
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Type-safe fetch for Next.js — zero runtime, zero config, zero dependencies.
|
|
4
4
|
|
|
5
|
+
[](https://codesandbox.io/p/sandbox/github/caocchinh/next-zero-rpc/tree/main/examples/minimal)
|
|
6
|
+
|
|
5
7
|
```bash
|
|
6
8
|
npx next-zero-rpc init
|
|
7
9
|
```
|
|
@@ -38,30 +40,46 @@ if (err) {
|
|
|
38
40
|
|
|
39
41
|
### How it compares
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
| Type-safe
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
43
|
+
> The table below reflects the libraries as of mid-2025. tRPC and ts-rest are both also zero-dependency at their core — the differentiators here are _approach_ and _what you give up_, not dependency counts.
|
|
44
|
+
|
|
45
|
+
| Feature | next-zero-rpc | tRPC | ts-rest | raw fetch |
|
|
46
|
+
| ------------------------------- | ------------------- | -------------------- | ----------------- | --------- |
|
|
47
|
+
| Type-safe paths | ✅ | ✅ | ✅ | ❌ |
|
|
48
|
+
| Type-safe responses | ✅ | ✅ | ✅ | ❌ |
|
|
49
|
+
| Type-safe methods | ✅ | N/A (procedures) | ✅ | ❌ |
|
|
50
|
+
| **Per-route error narrowing** | ✅ | ❌ | ❌ | ❌ |
|
|
51
|
+
| Client runtime size | ~1.8 KB | ~15 KB | comparable | 0 |
|
|
52
|
+
| Standard Next.js route handlers | ✅ (no changes) | ❌ (use tRPC router) | ❌ (use contract) | ✅ |
|
|
53
|
+
| Dynamic params `[id]` | ✅ | ✅ | ✅ | N/A |
|
|
54
|
+
| Catch-all `[...slug]` | ✅ | ✅ | ✅ | N/A |
|
|
55
|
+
| Go-style error handling | ✅ | ❌ | ❌ | ❌ |
|
|
56
|
+
| Exhaustive error checking | ✅ | ❌ | ❌ | ❌ |
|
|
57
|
+
| Server action helpers | ✅ | ❌ | N/A | N/A |
|
|
58
|
+
| Input validation built-in | ❌ (bring your own) | ✅ (Zod pipeline) | ✅ (Zod contract) | ❌ |
|
|
59
|
+
| OpenAPI / non-TS client support | ❌ | ❌ (plugin needed) | ✅ (core feature) | ❌ |
|
|
60
|
+
| Middleware / request pipeline | ❌ | ✅ | partial | ❌ |
|
|
61
|
+
| Subscriptions / WebSockets | ❌ | ✅ | ❌ | ❌ |
|
|
62
|
+
| Ecosystem maturity | early (v0.1.x) | large | solid | N/A |
|
|
63
|
+
| Core runtime dependencies | 0 | 0 | 0 | 0 |
|
|
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
|
|
72
|
+
- You're building a solo or small-team Next.js project and prefer to own a few simple files over maintaining a dependency
|
|
73
|
+
|
|
56
74
|
|
|
57
75
|
## Philosophy
|
|
58
76
|
|
|
59
77
|
**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.
|
|
60
78
|
|
|
61
|
-
- **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.
|
|
79
|
+
- **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.
|
|
62
80
|
- **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.
|
|
63
81
|
- **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.
|
|
64
|
-
- **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
|
|
82
|
+
- **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.
|
|
65
83
|
- **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.
|
|
66
84
|
|
|
67
85
|
## Setup
|
|
@@ -175,7 +193,7 @@ This works because:
|
|
|
175
193
|
|
|
176
194
|
1. `createApiError` is generic: `createApiError<C extends ErrorCode>(code: C, ...) → NextResponse<ApiErrorPayload<C>>`
|
|
177
195
|
2. TypeScript infers the literal `C` from each call site in your handler
|
|
178
|
-
3. `
|
|
196
|
+
3. `UnwrapNextResponse` extracts the union of all `ApiErrorPayload<C>` types from the handler's return type
|
|
179
197
|
4. The client sees only those specific error codes
|
|
180
198
|
|
|
181
199
|
### Go-Style Tuple Returns
|
|
@@ -239,6 +257,18 @@ const [data, err] = await apiFetch("/api/extreme/org1/projects/proj1/tasks/a/b/c
|
|
|
239
257
|
const [data, err] = await apiFetch("/api/users/123?include=profile", { method: "GET" });
|
|
240
258
|
```
|
|
241
259
|
|
|
260
|
+
### Static vs Dynamic Route Precedence
|
|
261
|
+
|
|
262
|
+
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.
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// Safely infers the type of the `active` route, completely ignoring the `[userId]` route
|
|
266
|
+
const [activeUsers] = await apiFetch("/api/users/active", { method: "GET" });
|
|
267
|
+
|
|
268
|
+
// Safely infers the type of the `[userId]` route
|
|
269
|
+
const [singleUser] = await apiFetch("/api/users/123", { method: "GET" });
|
|
270
|
+
```
|
|
271
|
+
|
|
242
272
|
### Route Groups Support
|
|
243
273
|
|
|
244
274
|
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:
|
|
@@ -367,15 +397,15 @@ HTTP_STATUS_ERROR.GATEWAY_TIMEOUT; // 504
|
|
|
367
397
|
|
|
368
398
|
#### Functions
|
|
369
399
|
|
|
370
|
-
| Function | Signature
|
|
371
|
-
| ------------------------- |
|
|
372
|
-
| `createApiError<C>` | `(code: C, statusCode, options?) → NextResponse<ApiErrorPayload<C>>`
|
|
373
|
-
| `createApiSuccess<T>` | `(data: T, statusCode?) → NextResponse<T>`
|
|
374
|
-
| `createApiSuccess` | `(undefined, NO_CONTENT) → NextResponse<undefined>`
|
|
375
|
-
| `isApiErrorPayload` | `(payload: unknown) → payload is ApiErrorPayload<ErrorCode>`
|
|
376
|
-
| `createServiceError` | `(code, options?) → [null, ServiceError]`
|
|
377
|
-
| `createServiceSuccess<T>` | `(data?: T) → [T \| undefined, null]`
|
|
378
|
-
| `assertNever` | `(value: never) → never`
|
|
400
|
+
| Function | Signature | Description |
|
|
401
|
+
| ------------------------- | -------------------------------------------------------------------- | ------------------------------------- |
|
|
402
|
+
| `createApiError<C>` | `(code: C, statusCode, options?) → NextResponse<ApiErrorPayload<C>>` | Create a typed API error response |
|
|
403
|
+
| `createApiSuccess<T>` | `(data: T, statusCode?) → NextResponse<T>` | Create a typed API success response |
|
|
404
|
+
| `createApiSuccess` | `(undefined, NO_CONTENT) → NextResponse<undefined>` | Overload for 204 No Content |
|
|
405
|
+
| `isApiErrorPayload` | `(payload: unknown) → payload is ApiErrorPayload<ErrorCode>` | Runtime type guard for error payloads |
|
|
406
|
+
| `createServiceError` | `(code, options?) → [null, ServiceError]` | Go-style error for server actions |
|
|
407
|
+
| `createServiceSuccess<T>` | `(data?: T) → [T \| undefined, null]` | Go-style success for server actions |
|
|
408
|
+
| `assertNever` | `(value: never) → never` | Compile-time exhaustiveness guard |
|
|
379
409
|
|
|
380
410
|
### `apiClient.ts`
|
|
381
411
|
|
|
@@ -507,6 +537,24 @@ Since you own all the source files, you can customize anything:
|
|
|
507
537
|
- **Change the output path** — Update `REGISTRY_FILE` in `update-api-registry.mjs`
|
|
508
538
|
- **Override the generator** — The `withApiRegistry` plugin and `updateApiRegistry()` function are fully yours to modify
|
|
509
539
|
|
|
540
|
+
## Examples
|
|
541
|
+
|
|
542
|
+
### Try it online
|
|
543
|
+
|
|
544
|
+
[](https://codesandbox.io/p/sandbox/github/caocchinh/next-zero-rpc/tree/main/examples/minimal)
|
|
545
|
+
|
|
546
|
+
Opens [`examples/minimal`](./examples/minimal) — a self-contained Next.js app with a working `apiFetch` client and two example API routes.
|
|
547
|
+
|
|
548
|
+
### Run locally
|
|
549
|
+
|
|
550
|
+
```bash
|
|
551
|
+
# Clone just the example folder (no git history)
|
|
552
|
+
npx degit caocchinh/next-zero-rpc/examples/minimal my-app
|
|
553
|
+
cd my-app
|
|
554
|
+
npm install
|
|
555
|
+
npm run dev
|
|
556
|
+
```
|
|
557
|
+
|
|
510
558
|
## CLI Usage
|
|
511
559
|
|
|
512
560
|
```bash
|
|
@@ -518,9 +566,11 @@ npx next-zero-rpc --help # Show help
|
|
|
518
566
|
|
|
519
567
|
## Requirements
|
|
520
568
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
569
|
+
| Requirement | Minimum Version | Reason |
|
|
570
|
+
| ----------- | --------------- | ------------------------------------------ |
|
|
571
|
+
| Next.js | **14.0** | App Router (stable since 14.0) |
|
|
572
|
+
| TypeScript | **4.9** | `satisfies` keyword used in `responses.ts` |
|
|
573
|
+
| Node.js | **18** | Native `fetch` API required by `apiFetch` |
|
|
524
574
|
|
|
525
575
|
## License
|
|
526
576
|
|
package/package.json
CHANGED
package/templates/apiClient.ts
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/templates/apiRegistry.ts
CHANGED
|
@@ -37,11 +37,13 @@ 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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
export type FindMatchingRoute<Path extends string> = Path extends keyof KnownRoutes
|
|
41
|
+
? Path
|
|
42
|
+
: {
|
|
43
|
+
[K in keyof KnownRoutes & string]: MatchSegments<Split<StripQuery<Path>>, Split<K>> extends true
|
|
44
|
+
? K
|
|
45
|
+
: never;
|
|
46
|
+
}[keyof KnownRoutes & string];
|
|
45
47
|
|
|
46
48
|
export type CheckPath<Path extends string> = Path extends ""
|
|
47
49
|
? keyof KnownRoutes
|