Installation
Deploy the type-safe bridge into your architecture.
Scaffold Core Files
Use the CLI to scaffold everything, or copy the files manually.
$ npx next-zero-rpc initCreates lib/next-zero-rpc/ and generates the initial apiRegistry.ts automatically. Skip to Step 02.
Setup Next.js Watcher
Inject the plugin so the registry auto-updates on every route change during dev. Optional — applies to both CLI and manual installs.
import { withApiRegistry } from "./src/lib/next-zero-rpc/update-api-registry.mjs";
const nextConfig = {};
export default withApiRegistry(nextConfig);
Skip this step? Just run npm run infer-api manually whenever you add or change API routes.
Use Cases
Core patterns and daily workflows.
Backend: Route Handlers
Use createApiSuccess and createApiError instead of raw Next.js responses. The error codes must be literal strings.
import { createApiSuccess, createApiError, HTTP_STATUS_ERROR } from "@/lib/next-zero-rpc/responses";
export async function GET() {
const user = await db.getUser();
if (!user) {
return createApiError("resource:not-found", HTTP_STATUS_ERROR.NOT_FOUND);
}
return createApiSuccess({ id: user.id, name: user.name });
}Frontend: Client Fetch
Fetch data using Go-style tuples. apiFetch guarantees type safety across the network boundary, including autocomplete for route paths.
"use client";
import { apiFetch } from "@/lib/next-zero-rpc/apiClient";
const [data, err] = await apiFetch("/api/users", { method: "GET" });
if (err) {
console.error("Failed:", err.message);
return;
}
// Fully typed!
console.log(data.name, data.id);Exhaustive Error Handling
TypeScript narrows err.code to exactly the errors your route throws. Use assertNever to catch unhandled errors at compile time.
import { apiFetch } from "@/lib/next-zero-rpc/apiClient";
import { assertNever } from "@/lib/next-zero-rpc/responses";
const [data, err] = await apiFetch("/api/users", { method: "GET" });
if (err) {
switch (err.code) {
case "resource:not-found":
showToast("User missing");
break;
case "system:unknown-error": // Always explicitly handle network errors
showToast("Network error");
break;
default:
assertNever(err.code); // TS Error if you forget a case!
}
}Server Actions & Services
For backend functions that aren't API Routes (like Server Actions), use the createServiceSuccess and createServiceError helpers for the same tuple-based DX.
"use server";
import { createServiceSuccess, createServiceError } from "@/lib/next-zero-rpc/responses";
export async function updateUser(id: string, data: any) {
if (!id) {
return createServiceError("validation:missing-required-fields");
}
await db.update(id, data);
return createServiceSuccess({ success: true });
}Dynamic Path Segments
Pass runtime variables directly into the path via template literals. apiFetch resolves them against your route registry at compile time — single or multiple dynamic segments are equally type-safe.
import { apiFetch } from "@/lib/next-zero-rpc/apiClient";
// Single dynamic segment
const userId = "abc-123";
const [user] = await apiFetch(`/api/users/${userId}`, { method: "GET" });
// Multiple dynamic segments across nested routes
const orgId = "org-1";
const projectId = "proj-42";
const taskId = "task-99";
const [task] = await apiFetch(
`/api/orgs/${orgId}/projects/${projectId}/tasks/${taskId}`,
{ method: "PATCH" }
);Method Narrowing
The method option is narrowed to only the HTTP verbs your route actually exports. Passing an unsupported method is a TypeScript compile error — caught before it ever hits the network.
// app/api/posts/route.ts — only GET and POST are exported
export async function GET() { ... }
export async function POST() { ... }
// ✅ Valid — method matches an export
await apiFetch("/api/posts", { method: "GET" });
await apiFetch("/api/posts", { method: "POST" });
// ❌ TypeScript compile error — DELETE is not exported from this route
await apiFetch("/api/posts", { method: "DELETE" });
// ^^^^^^^^
// Argument of type '"DELETE"' is not assignable to parameter of type '"GET" | "POST"'Route Groups & Query Strings
Next.js route groups like (admin) are invisible to the type system — omit them in fetch paths. Query strings are stripped before validation, so append them freely without breaking type safety.
import { apiFetch } from "@/lib/next-zero-rpc/apiClient";
// File lives at: app/api/(admin)/users/route.ts
// Fetch path: /api/users (route group is stripped)
const [users] = await apiFetch("/api/users", { method: "GET" });
// Query strings are stripped before path validation — fully type-safe
const [active] = await apiFetch("/api/users?active=true&role=admin", { method: "GET" });