rpc4next
v0.6.1
Published
Inspired by Hono RPC and Pathpida, rpc4next brings a lightweight and intuitive RPC solution to Next.js, making server-client communication seamless
Readme
rpc4next
rpc4next is a lightweight, type-safe RPC layer for Next.js App Router projects.
It scans your existing app/** files, generates a PathStructure type, and lets you call route handlers through a typed client without introducing a custom server framework.
It is inspired by Hono RPC and Pathpida:
route.tsfiles become typed RPC endpointspage.tsxfiles become typed URL/path entries- dynamic segments and exported route
Querytypes are reflected in generated client types - optional generated
params.tsfiles can give route files a stable siblingParamstype
If you want to see a full working example, start with the real integration fixture in integration/next-app/README.md. It shows how route scanning, generated types, the client, and a real Next.js app fit together in this repository.
What It Covers
- Typed client calls for
app/**/route.ts - Typed URL generation for
app/**/page.tsx - Dynamic routes, catch-all routes, and optional catch-all routes
- Route groups and parallel-route descendants
- Validation helpers for
params,query,json,headers, andcookies - Plain Next.js route handlers written with
NextResponse.json(...)orResponse.json(...)
Routing notes:
- Route group folders do not appear in generated public paths
- Parallel route slot names are excluded, but their descendant pages are flattened onto public URL paths
- Intercepting route branches are excluded from
PathStructurebecause rpc4next models public URL paths
This is a good fit if you want typed client calls and typed URLs from an existing App Router codebase without moving to a custom RPC server framework. If you already want to keep writing normal route.ts and page.tsx files, rpc4next is designed for that.
Requirements
- Node.js
>=20.19.2 - Next.js App Router
- Package peer dependency support in
rpc4nextandrpc4next-cli: Next.js^15or^16
Installation
npm install rpc4next
npm install -D rpc4next-cliIf you use Bun in your project:
bun add rpc4next
bun add -d rpc4next-clizod is only needed if you use the server-side validation helpers such as
zValidator(). If you only use the generated client types and do not validate
request input with Zod, you can omit it.
If you want Zod-based request validation later:
npm install zodQuick Start
If you prefer to inspect a complete app before wiring this into your own project, see integration/next-app/README.md.
1. Define a Route
rpc4next does not require routeHandlerFactory().
It can scan and generate client types from standard Next.js App Router handlers as-is.
The server helpers are optional and mainly give you stronger response and validation typing.
If you want the stronger typed server-side experience, use routeHandlerFactory():
// app/api/users/[userId]/route.ts
import { routeHandlerFactory } from "rpc4next/server";
export type Query = {
includePosts?: "true" | "false";
};
const createRouteHandler = routeHandlerFactory();
export const { GET } = createRouteHandler<{
params: { userId: string };
query: Query;
}>().get(async (rc) => {
const { userId } = await rc.req.params();
const query = rc.req.query();
return rc.json({
ok: true,
userId,
includePosts: query.includePosts === "true",
});
});Notes:
routeHandlerFactory()is optional, not required- Export
Queryfrom a route if you want the generated client to typesearchParamsfor plain Next.js handlers too routeHandlerFactory()gives you typed helpers such asrc.json(),rc.text(), andrc.redirect()- Validation helpers such as
zValidator()are optional
If you also want request validation with Zod, add zValidator():
import { routeHandlerFactory } from "rpc4next/server";
import { zValidator } from "rpc4next/server/validators/zod";
import { z } from "zod";
const createRouteHandler = routeHandlerFactory();
const querySchema = z.object({
includePosts: z.enum(["true", "false"]).optional(),
});
export const { GET } = createRouteHandler<{
params: { userId: string };
query: z.infer<typeof querySchema>;
}>().get(zValidator("query", querySchema), async (rc) => {
const query = rc.req.valid("query");
return rc.json({ ok: true, includePosts: query.includePosts === "true" });
});zValidator() validates request input and returns 400 JSON errors by default on invalid input.
2. Generate PathStructure
Generate the client types from your app directory:
npx rpc4next app src/generated/rpc.tsIf you use Bun:
bunx rpc4next app src/generated/rpc.tsYou can also configure the CLI with rpc4next.config.json:
{
"baseDir": "app",
"outputPath": "src/generated/rpc.ts",
"paramsFile": "params.ts"
}Then run:
npx rpc4nextOr with Bun:
bunx rpc4nextPositional arguments:
<baseDir>: the App Router root to scan, such asapp<outputPath>: the file to generate, such assrc/generated/rpc.ts
Useful options:
-w,--watch: regenerate on file changes-p,--params-file [filename]: generate sibling params files such asapp/users/[userId]/params.ts
Examples:
npx rpc4next --watch
npx rpc4next app src/generated/rpc.ts --params-file params.ts3. Create a Client
// src/lib/rpc-client.ts
import { createRpcClient } from "rpc4next/client";
import type { PathStructure } from "../generated/rpc";
export const rpc = createRpcClient<PathStructure>("");Use "" for same-origin calls in the browser, or pass an absolute base URL for server-side or cross-origin usage.
4. Call Routes
Generated client naming follows the App Router path shape:
- static segments stay as property access, such as
rpc.api.users - dynamic segments become callable helpers, such as
[userId] -> ._userId("123") route.tsmethods become$get(),$post(), and so onpage.tsxentries can be turned into typed URLs with$url()
const response = await rpc.api.users._userId("123").$get({
url: { query: { includePosts: "true" } },
});
const data = await response.json();For JSON request bodies:
const response = await rpc.api.posts.$post({
body: { json: { title: "hello" } },
});For request headers and cookies:
const response = await rpc.api["request-meta"].$get({
requestHeaders: {
headers: { "x-integration-test": "example" },
cookies: { session: "abc123" },
},
});5. Generate Typed URLs for Pages
page.tsx files are included in the generated path tree, so you can build typed URLs even when there is no RPC method to call.
const photoUrl = rpc.photo._id("42").$url();
photoUrl.path;
photoUrl.relativePath;
photoUrl.pathname;
photoUrl.params;Server Helpers
routeHandlerFactory
routeHandlerFactory() creates typed handlers for:
getpostputdeletepatchheadoptions
It also supports a shared error handler:
import { routeHandlerFactory } from "rpc4next/server";
const createRouteHandler = routeHandlerFactory((error, rc) => {
return rc.text("error", 400);
});
export const { POST } = createRouteHandler().post(async (rc) => {
return rc.json({ ok: true }, 201);
});zValidator
zValidator() supports these targets:
paramsqueryjsonheaderscookies
Example:
import { routeHandlerFactory } from "rpc4next/server";
import { zValidator } from "rpc4next/server/validators/zod";
import { z } from "zod";
const createRouteHandler = routeHandlerFactory();
const jsonSchema = z.object({
title: z.string().min(1),
});
export const { POST } = createRouteHandler().post(
zValidator("json", jsonSchema),
async (rc) => {
const body = rc.req.valid("json");
return rc.json({ title: body.title }, 201);
},
);If you provide a custom hook, you must return a response yourself when validation fails:
zValidator("json", jsonSchema, (result, rc) => {
if (!result.success) {
return rc.json({ error: result.error.issues }, 422);
}
});Plain Next.js Route Handlers Also Work
You can keep using native App Router handlers without adopting routeHandlerFactory().
This is useful when you want to stay close to stock Next.js APIs and only use rpc4next for route scanning and client generation.
Example with NextResponse.json(...):
// app/api/next-native/[itemId]/route.ts
import { type NextRequest, NextResponse } from "next/server";
export type Query = {
filter?: string;
};
export async function GET(
request: NextRequest,
context: { params: Promise<{ itemId: string }> },
) {
const { itemId } = await context.params;
const filter = request.nextUrl.searchParams.get("filter") ?? "all";
return NextResponse.json({
ok: true,
itemId,
filter,
});
}Example with Response.json(...):
// app/api/next-native-response/route.ts
export async function GET() {
return Response.json({
ok: true,
source: "response-json",
});
}The generated client can still call this route:
const response = await rpc.api["next-native"]
._itemId("item-1")
.$get({ url: { query: { filter: "recent" } } });You can also call a plain Response.json(...) route:
const response = await rpc.api["next-native-response"].$get();For native handlers, route discovery and request typing still work, but response typing is naturally broader than when you return rpc4next's typed helpers.
See integration/next-app/README.md for the repository's full integration fixture coverage and route-pattern notes.
Generated Files
When paramsFile is enabled, the CLI can generate sibling files such as:
// app/api/users/[userId]/params.ts
export type Params = { userId: string };That lets route files import the param shape instead of repeating it manually.
These generated params.ts files are optional, and your generated src/generated/rpc.ts is typically not something you edit by hand.
Your generated src/generated/rpc.ts exports a PathStructure type that includes:
- path entries from
page.tsx - callable HTTP methods from
route.ts - dynamic segment parameter types
- route
Queryexports where available
Typical Workflow
- Add or update files under
app/** - Run
rpc4nextto regeneratePathStructure - Import
PathStructureinto your client - Call routes with
createRpcClient<PathStructure>(...) - Use
routeHandlerFactoryandzValidatorwhere you want stronger server-side typing
Repository Layout
packages/rpc4next: runtime client and server helperspackages/rpc4next-cli: route scanner and type generatorpackages/rpc4next-shared: internal shared constants and typesintegration/next-app: real Next.js integration fixture
If you are evaluating the repository itself, integration/next-app is the best place to see the full flow working in a real app.
License
MIT
