rpc4next 0.5.0 → 0.6.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
CHANGED
|
@@ -2,331 +2,413 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://deepwiki.com/watanabe-1/rpc4next)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`rpc4next` is a lightweight, type-safe RPC layer for Next.js App Router projects.
|
|
6
|
+
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.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
It is inspired by Hono RPC and Pathpida:
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
- `route.ts` files become typed RPC endpoints
|
|
11
|
+
- `page.tsx` files become typed URL/path entries
|
|
12
|
+
- dynamic segments and exported route `Query` types are reflected in generated client types
|
|
13
|
+
- optional generated `params.ts` files can give route files a stable sibling `Params` type
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
If you want to see a full working example, start with the real integration fixture in [integration/next-app/README.md](./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.
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
- ✅ 既存の `app/**/route.ts` および `app/**/page.tsx` を活用するため、新たなハンドラファイルの作成は不要
|
|
15
|
-
- ✅ 最小限のセットアップで、カスタムサーバー不要
|
|
16
|
-
- ✅ 動的ルート(`[id]`、`[...slug]` など)に対応
|
|
17
|
-
- ✅ CLI による自動クライアント用型定義生成
|
|
17
|
+
## What It Covers
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
- Typed client calls for `app/**/route.ts`
|
|
20
|
+
- Typed URL generation for `app/**/page.tsx`
|
|
21
|
+
- Dynamic routes, catch-all routes, and optional catch-all routes
|
|
22
|
+
- Route groups and parallel-route descendants
|
|
23
|
+
- Validation helpers for `params`, `query`, `json`, `headers`, and `cookies`
|
|
24
|
+
- Plain Next.js route handlers written with `NextResponse.json(...)` or `Response.json(...)`
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
Routing notes:
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
- Route group folders do not appear in generated public paths
|
|
29
|
+
- Parallel route slot names are excluded, but their descendant pages are flattened onto public URL paths
|
|
30
|
+
- Intercepting route branches are excluded from `PathStructure` because rpc4next models public URL paths
|
|
31
|
+
|
|
32
|
+
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.
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
- Node.js `>=20.19.2`
|
|
37
|
+
- Next.js App Router
|
|
38
|
+
- Package peer dependency support in `rpc4next` and `rpc4next-cli`: Next.js `^14`, `^15`, or `^16`
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
24
41
|
|
|
25
42
|
```bash
|
|
26
43
|
npm install rpc4next
|
|
27
44
|
npm install -D rpc4next-cli
|
|
28
45
|
```
|
|
29
46
|
|
|
30
|
-
|
|
47
|
+
If you use Bun in your project:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
bun add rpc4next
|
|
51
|
+
bun add -d rpc4next-cli
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`zod` is only needed if you use the server-side validation helpers such as
|
|
55
|
+
`zValidator()`. If you only use the generated client types and do not validate
|
|
56
|
+
request input with Zod, you can omit it.
|
|
57
|
+
|
|
58
|
+
If you want Zod-based request validation later:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm install zod
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
If you prefer to inspect a complete app before wiring this into your own project, see [integration/next-app/README.md](./integration/next-app/README.md).
|
|
31
67
|
|
|
32
|
-
|
|
33
|
-
|
|
68
|
+
### 1. Define a Route
|
|
69
|
+
|
|
70
|
+
`rpc4next` does not require `routeHandlerFactory()`.
|
|
71
|
+
It can scan and generate client types from standard Next.js App Router handlers as-is.
|
|
72
|
+
The server helpers are optional and mainly give you stronger response and validation typing.
|
|
73
|
+
|
|
74
|
+
If you want the stronger typed server-side experience, use `routeHandlerFactory()`:
|
|
34
75
|
|
|
35
76
|
```ts
|
|
36
|
-
// app/api/
|
|
37
|
-
import {
|
|
77
|
+
// app/api/users/[userId]/route.ts
|
|
78
|
+
import { routeHandlerFactory } from "rpc4next/server";
|
|
38
79
|
|
|
39
|
-
// searchParams用の型定義
|
|
40
80
|
export type Query = {
|
|
41
|
-
|
|
42
|
-
page?: number; // 任意
|
|
81
|
+
includePosts?: "true" | "false";
|
|
43
82
|
};
|
|
44
83
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
84
|
+
const createRouteHandler = routeHandlerFactory();
|
|
85
|
+
|
|
86
|
+
export const { GET } = createRouteHandler<{
|
|
87
|
+
params: { userId: string };
|
|
88
|
+
query: Query;
|
|
89
|
+
}>().get(async (rc) => {
|
|
90
|
+
const { userId } = await rc.req.params();
|
|
91
|
+
const query = rc.req.query();
|
|
92
|
+
|
|
93
|
+
return rc.json({
|
|
94
|
+
ok: true,
|
|
95
|
+
userId,
|
|
96
|
+
includePosts: query.includePosts === "true",
|
|
97
|
+
});
|
|
98
|
+
});
|
|
53
99
|
```
|
|
54
100
|
|
|
55
|
-
|
|
101
|
+
Notes:
|
|
56
102
|
|
|
57
|
-
-
|
|
103
|
+
- `routeHandlerFactory()` is optional, not required
|
|
104
|
+
- Export `Query` from a route if you want the generated client to type `searchParams` for plain Next.js handlers too
|
|
105
|
+
- `routeHandlerFactory()` gives you typed helpers such as `rc.json()`, `rc.text()`, and `rc.redirect()`
|
|
106
|
+
- Validation helpers such as `zValidator()` are optional
|
|
58
107
|
|
|
59
|
-
|
|
108
|
+
If you also want request validation with Zod, add `zValidator()`:
|
|
60
109
|
|
|
61
|
-
|
|
110
|
+
```ts
|
|
111
|
+
import { routeHandlerFactory } from "rpc4next/server";
|
|
112
|
+
import { zValidator } from "rpc4next/server/validators/zod";
|
|
113
|
+
import { z } from "zod";
|
|
62
114
|
|
|
63
|
-
|
|
115
|
+
const createRouteHandler = routeHandlerFactory();
|
|
64
116
|
|
|
65
|
-
|
|
66
|
-
|
|
117
|
+
const querySchema = z.object({
|
|
118
|
+
includePosts: z.enum(["true", "false"]).optional(),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export const { GET } = createRouteHandler<{
|
|
122
|
+
params: { userId: string };
|
|
123
|
+
query: z.infer<typeof querySchema>;
|
|
124
|
+
}>().get(zValidator("query", querySchema), async (rc) => {
|
|
125
|
+
const query = rc.req.valid("query");
|
|
126
|
+
return rc.json({ ok: true, includePosts: query.includePosts === "true" });
|
|
127
|
+
});
|
|
67
128
|
```
|
|
68
129
|
|
|
69
|
-
`
|
|
130
|
+
`zValidator()` validates request input and returns `400` JSON errors by default on invalid input.
|
|
70
131
|
|
|
71
|
-
|
|
72
|
-
- `<outputPath>`: 生成された型定義ファイルの出力先
|
|
132
|
+
### 2. Generate `PathStructure`
|
|
73
133
|
|
|
74
|
-
|
|
134
|
+
Generate the client types from your `app` directory:
|
|
75
135
|
|
|
76
|
-
|
|
77
|
-
|
|
136
|
+
```bash
|
|
137
|
+
npx rpc4next app src/generated/rpc.ts
|
|
138
|
+
```
|
|
78
139
|
|
|
79
|
-
|
|
80
|
-
npx rpc4next <baseDir> <outputPath> --watch
|
|
81
|
-
```
|
|
140
|
+
If you use Bun:
|
|
82
141
|
|
|
83
|
-
|
|
84
|
-
|
|
142
|
+
```bash
|
|
143
|
+
bunx rpc4next app src/generated/rpc.ts
|
|
144
|
+
```
|
|
85
145
|
|
|
86
|
-
|
|
87
|
-
npx rpc4next <baseDir> <outputPath> --params-file <paramsFileName>
|
|
88
|
-
```
|
|
146
|
+
You can also configure the CLI with `rpc4next.config.json`:
|
|
89
147
|
|
|
90
|
-
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"baseDir": "app",
|
|
151
|
+
"outputPath": "src/generated/rpc.ts",
|
|
152
|
+
"paramsFile": "params.ts"
|
|
153
|
+
}
|
|
154
|
+
```
|
|
91
155
|
|
|
92
|
-
|
|
156
|
+
Then run:
|
|
93
157
|
|
|
94
|
-
|
|
158
|
+
```bash
|
|
159
|
+
npx rpc4next
|
|
160
|
+
```
|
|
95
161
|
|
|
96
|
-
|
|
97
|
-
// lib/rpcClient.ts
|
|
98
|
-
import { createClient } from "rpc4next/client";
|
|
99
|
-
import type { PathStructure } from "あなたが生成した型定義ファイル";
|
|
162
|
+
Or with Bun:
|
|
100
163
|
|
|
101
|
-
|
|
164
|
+
```bash
|
|
165
|
+
bunx rpc4next
|
|
102
166
|
```
|
|
103
167
|
|
|
104
|
-
|
|
168
|
+
Positional arguments:
|
|
105
169
|
|
|
106
|
-
|
|
170
|
+
- `<baseDir>`: the App Router root to scan, such as `app`
|
|
171
|
+
- `<outputPath>`: the file to generate, such as `src/generated/rpc.ts`
|
|
107
172
|
|
|
108
|
-
|
|
173
|
+
Useful options:
|
|
109
174
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
import { rpc } from "@/lib/rpcClient";
|
|
175
|
+
- `-w`, `--watch`: regenerate on file changes
|
|
176
|
+
- `-p`, `--params-file [filename]`: generate sibling params files such as `app/users/[userId]/params.ts`
|
|
113
177
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return <div>{json.q}</div>;
|
|
120
|
-
}
|
|
178
|
+
Examples:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
npx rpc4next --watch
|
|
182
|
+
npx rpc4next app src/generated/rpc.ts --params-file params.ts
|
|
121
183
|
```
|
|
122
184
|
|
|
123
|
-
|
|
124
|
-
- リクエストの構造(params, query)はサーバーコードから推論され、レスポンスも型安全に扱えます。
|
|
185
|
+
### 3. Create a Client
|
|
125
186
|
|
|
126
|
-
|
|
187
|
+
```ts
|
|
188
|
+
// src/lib/rpc-client.ts
|
|
189
|
+
import { createRpcClient } from "rpc4next/client";
|
|
190
|
+
import type { PathStructure } from "../generated/rpc";
|
|
127
191
|
|
|
128
|
-
|
|
192
|
+
export const rpc = createRpcClient<PathStructure>("");
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Use `""` for same-origin calls in the browser, or pass an absolute base URL for server-side or cross-origin usage.
|
|
129
196
|
|
|
130
|
-
###
|
|
197
|
+
### 4. Call Routes
|
|
131
198
|
|
|
132
|
-
|
|
133
|
-
- ステータス、Content-Type、Body がすべて型で保証される
|
|
134
|
-
- クライアントは受け取るレスポンス型を完全に推論可能
|
|
199
|
+
Generated client naming follows the App Router path shape:
|
|
135
200
|
|
|
136
|
-
|
|
137
|
-
|
|
201
|
+
- static segments stay as property access, such as `rpc.api.users`
|
|
202
|
+
- dynamic segments become callable helpers, such as `[userId] -> ._userId("123")`
|
|
203
|
+
- `route.ts` methods become `$get()`, `$post()`, and so on
|
|
204
|
+
- `page.tsx` entries can be turned into typed URLs with `$url()`
|
|
138
205
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
206
|
+
```ts
|
|
207
|
+
const response = await rpc.api.users._userId("123").$get({
|
|
208
|
+
url: { query: { includePosts: "true" } },
|
|
209
|
+
});
|
|
142
210
|
|
|
143
|
-
|
|
211
|
+
const data = await response.json();
|
|
212
|
+
```
|
|
144
213
|
|
|
145
|
-
|
|
214
|
+
For JSON request bodies:
|
|
146
215
|
|
|
147
216
|
```ts
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
);
|
|
217
|
+
const response = await rpc.api.posts.$post({
|
|
218
|
+
body: { json: { title: "hello" } },
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
For request headers and cookies:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
const response = await rpc.api["request-meta"].$get({
|
|
226
|
+
requestHeaders: {
|
|
227
|
+
headers: { "x-integration-test": "example" },
|
|
228
|
+
cookies: { session: "abc123" },
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 5. Generate Typed URLs for Pages
|
|
151
234
|
|
|
152
|
-
|
|
235
|
+
`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.
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const photoUrl = rpc.photo._id("42").$url();
|
|
239
|
+
|
|
240
|
+
photoUrl.path;
|
|
241
|
+
photoUrl.relativePath;
|
|
242
|
+
photoUrl.pathname;
|
|
243
|
+
photoUrl.params;
|
|
153
244
|
```
|
|
154
245
|
|
|
155
|
-
|
|
246
|
+
## Server Helpers
|
|
156
247
|
|
|
157
|
-
|
|
248
|
+
### `routeHandlerFactory`
|
|
158
249
|
|
|
159
|
-
|
|
250
|
+
`routeHandlerFactory()` creates typed handlers for:
|
|
160
251
|
|
|
161
|
-
|
|
252
|
+
- `get`
|
|
253
|
+
- `post`
|
|
254
|
+
- `put`
|
|
255
|
+
- `delete`
|
|
256
|
+
- `patch`
|
|
257
|
+
- `head`
|
|
258
|
+
- `options`
|
|
162
259
|
|
|
163
|
-
|
|
260
|
+
It also supports a shared error handler:
|
|
164
261
|
|
|
165
262
|
```ts
|
|
166
|
-
import {
|
|
167
|
-
import { zValidator } from "@/path/to/zValidator";
|
|
168
|
-
import { z } from "zod";
|
|
263
|
+
import { routeHandlerFactory } from "rpc4next/server";
|
|
169
264
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
userId: z.string(),
|
|
265
|
+
const createRouteHandler = routeHandlerFactory((error, rc) => {
|
|
266
|
+
return rc.text("error", 400);
|
|
173
267
|
});
|
|
174
268
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}>().get(
|
|
179
|
-
zValidator("params", paramsSchema), // paramsを検証
|
|
180
|
-
async (rc) => {
|
|
181
|
-
const params = rc.req.valid("params"); // バリデーション済みparamsを取得
|
|
182
|
-
return rc.json({ message: `User ID is ${params.userId}` });
|
|
183
|
-
},
|
|
184
|
-
);
|
|
269
|
+
export const { POST } = createRouteHandler().post(async (rc) => {
|
|
270
|
+
return rc.json({ ok: true }, 201);
|
|
271
|
+
});
|
|
185
272
|
```
|
|
186
273
|
|
|
187
|
-
|
|
274
|
+
### `zValidator`
|
|
188
275
|
|
|
189
|
-
|
|
276
|
+
`zValidator()` supports these targets:
|
|
190
277
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
| `cookies` | クッキー |
|
|
197
|
-
| `json` | リクエストボディ (Content-Type: `application/json`) |
|
|
278
|
+
- `params`
|
|
279
|
+
- `query`
|
|
280
|
+
- `json`
|
|
281
|
+
- `headers`
|
|
282
|
+
- `cookies`
|
|
198
283
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
### 🔥 複数ターゲットを同時に検証する例
|
|
284
|
+
Example:
|
|
202
285
|
|
|
203
286
|
```ts
|
|
204
|
-
import {
|
|
205
|
-
import { zValidator } from "
|
|
287
|
+
import { routeHandlerFactory } from "rpc4next/server";
|
|
288
|
+
import { zValidator } from "rpc4next/server/validators/zod";
|
|
206
289
|
import { z } from "zod";
|
|
207
290
|
|
|
208
|
-
const
|
|
209
|
-
page: z.string().regex(/^\d+$/),
|
|
210
|
-
});
|
|
291
|
+
const createRouteHandler = routeHandlerFactory();
|
|
211
292
|
|
|
212
293
|
const jsonSchema = z.object({
|
|
213
|
-
|
|
214
|
-
age: z.number(),
|
|
294
|
+
title: z.string().min(1),
|
|
215
295
|
});
|
|
216
296
|
|
|
217
|
-
export const { POST } = createRouteHandler
|
|
218
|
-
query: z.infer<typeof querySchema>;
|
|
219
|
-
}>().post(
|
|
220
|
-
zValidator("query", querySchema),
|
|
297
|
+
export const { POST } = createRouteHandler().post(
|
|
221
298
|
zValidator("json", jsonSchema),
|
|
222
299
|
async (rc) => {
|
|
223
|
-
const query = rc.req.valid("query");
|
|
224
300
|
const body = rc.req.valid("json");
|
|
225
|
-
return rc.json({
|
|
301
|
+
return rc.json({ title: body.title }, 201);
|
|
226
302
|
},
|
|
227
303
|
);
|
|
228
304
|
```
|
|
229
305
|
|
|
230
|
-
|
|
231
|
-
- **成功時は型安全に取得可能** (`rc.req.valid('query')`, `rc.req.valid('json')`)
|
|
232
|
-
|
|
233
|
-
---
|
|
234
|
-
|
|
235
|
-
これにより,クライアント側とサーバー側が、全面的に**型でつながる**ので,ミスを何次も防げ,開発体験を大幅に向上できます。
|
|
236
|
-
|
|
237
|
-
---
|
|
238
|
-
|
|
239
|
-
### ⚡ バリデーション失敗時のカスタムエラーハンドリング
|
|
240
|
-
|
|
241
|
-
- デフォルトでは、バリデーション失敗時に自動で `400 Bad Request` を返します。
|
|
242
|
-
- 必要に応じて、**カスタムフック**でエラー対応を制御できます。
|
|
306
|
+
If you provide a custom hook, you must return a response yourself when validation fails:
|
|
243
307
|
|
|
244
308
|
```ts
|
|
245
|
-
zValidator("
|
|
309
|
+
zValidator("json", jsonSchema, (result, rc) => {
|
|
246
310
|
if (!result.success) {
|
|
247
|
-
return rc.json({ error: result.error.
|
|
311
|
+
return rc.json({ error: result.error.issues }, 422);
|
|
248
312
|
}
|
|
249
313
|
});
|
|
250
314
|
```
|
|
251
315
|
|
|
252
|
-
|
|
316
|
+
## Plain Next.js Route Handlers Also Work
|
|
253
317
|
|
|
254
|
-
|
|
318
|
+
You can keep using native App Router handlers without adopting `routeHandlerFactory()`.
|
|
319
|
+
This is useful when you want to stay close to stock Next.js APIs and only use `rpc4next` for route scanning and client generation.
|
|
255
320
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
`rpc4next`で作成したクライアントは、`createRouteHandler` と `zValidator` で作成したルートハンドラの内容にしたがって **params, query, headers, cookies, json** を型安全に送信できます。
|
|
259
|
-
|
|
260
|
-
例:
|
|
321
|
+
Example with `NextResponse.json(...)`:
|
|
261
322
|
|
|
262
323
|
```ts
|
|
263
|
-
|
|
264
|
-
import type
|
|
324
|
+
// app/api/next-native/[itemId]/route.ts
|
|
325
|
+
import { type NextRequest, NextResponse } from "next/server";
|
|
265
326
|
|
|
266
|
-
|
|
327
|
+
export type Query = {
|
|
328
|
+
filter?: string;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
export async function GET(
|
|
332
|
+
request: NextRequest,
|
|
333
|
+
context: { params: Promise<{ itemId: string }> },
|
|
334
|
+
) {
|
|
335
|
+
const { itemId } = await context.params;
|
|
336
|
+
const filter = request.nextUrl.searchParams.get("filter") ?? "all";
|
|
267
337
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
338
|
+
return NextResponse.json({
|
|
339
|
+
ok: true,
|
|
340
|
+
itemId,
|
|
341
|
+
filter,
|
|
272
342
|
});
|
|
343
|
+
}
|
|
344
|
+
```
|
|
273
345
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
// name: string;
|
|
284
|
-
// age: number;
|
|
285
|
-
// };
|
|
286
|
-
// }
|
|
287
|
-
} else {
|
|
288
|
-
const error = await res.json();
|
|
289
|
-
|
|
290
|
-
// ⚠️ バリデーションエラー時は次の型が推論されます
|
|
291
|
-
// const error:
|
|
292
|
-
// | SafeParseError<{
|
|
293
|
-
// page: string;
|
|
294
|
-
// }>
|
|
295
|
-
// | SafeParseError<{
|
|
296
|
-
// name: string;
|
|
297
|
-
// age: number;
|
|
298
|
-
// }>;
|
|
299
|
-
}
|
|
346
|
+
Example with `Response.json(...)`:
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
// app/api/next-native-response/route.ts
|
|
350
|
+
export async function GET() {
|
|
351
|
+
return Response.json({
|
|
352
|
+
ok: true,
|
|
353
|
+
source: "response-json",
|
|
354
|
+
});
|
|
300
355
|
}
|
|
301
356
|
```
|
|
302
357
|
|
|
303
|
-
|
|
304
|
-
- サーバー側の型定義に基づいて、**型のズレを防止**できます
|
|
358
|
+
The generated client can still call this route:
|
|
305
359
|
|
|
306
|
-
|
|
360
|
+
```ts
|
|
361
|
+
const response = await rpc.api["next-native"]
|
|
362
|
+
._itemId("item-1")
|
|
363
|
+
.$get({ url: { query: { filter: "recent" } } });
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
You can also call a plain `Response.json(...)` route:
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
const response = await rpc.api["next-native-response"].$get();
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
For native handlers, route discovery and request typing still work, but response typing is naturally broader than when you return rpc4next's typed helpers.
|
|
373
|
+
|
|
374
|
+
See [integration/next-app/README.md](./integration/next-app/README.md) for the repository's full integration fixture coverage and route-pattern notes.
|
|
375
|
+
|
|
376
|
+
## Generated Files
|
|
377
|
+
|
|
378
|
+
When `paramsFile` is enabled, the CLI can generate sibling files such as:
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
// app/api/users/[userId]/params.ts
|
|
382
|
+
export type Params = { userId: string };
|
|
383
|
+
```
|
|
307
384
|
|
|
308
|
-
|
|
385
|
+
That lets route files import the param shape instead of repeating it manually.
|
|
386
|
+
These generated `params.ts` files are optional, and your generated `src/generated/rpc.ts` is typically not something you edit by hand.
|
|
309
387
|
|
|
310
|
-
|
|
388
|
+
Your generated `src/generated/rpc.ts` exports a `PathStructure` type that includes:
|
|
311
389
|
|
|
312
|
-
|
|
390
|
+
- path entries from `page.tsx`
|
|
391
|
+
- callable HTTP methods from `route.ts`
|
|
392
|
+
- dynamic segment parameter types
|
|
393
|
+
- route `Query` exports where available
|
|
313
394
|
|
|
314
|
-
##
|
|
395
|
+
## Typical Workflow
|
|
315
396
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
- Lint all packages: `bun run lint`
|
|
397
|
+
1. Add or update files under `app/**`
|
|
398
|
+
2. Run `rpc4next` to regenerate `PathStructure`
|
|
399
|
+
3. Import `PathStructure` into your client
|
|
400
|
+
4. Call routes with `createRpcClient<PathStructure>(...)`
|
|
401
|
+
5. Use `routeHandlerFactory` and `zValidator` where you want stronger server-side typing
|
|
322
402
|
|
|
323
|
-
##
|
|
403
|
+
## Repository Layout
|
|
324
404
|
|
|
325
|
-
-
|
|
326
|
-
-
|
|
405
|
+
- `packages/rpc4next`: runtime client and server helpers
|
|
406
|
+
- `packages/rpc4next-cli`: route scanner and type generator
|
|
407
|
+
- `packages/rpc4next-shared`: internal shared constants and types
|
|
408
|
+
- `integration/next-app`: real Next.js integration fixture
|
|
327
409
|
|
|
328
|
-
|
|
410
|
+
If you are evaluating the repository itself, `integration/next-app` is the best place to see the full flow working in a real app.
|
|
329
411
|
|
|
330
|
-
##
|
|
412
|
+
## License
|
|
331
413
|
|
|
332
414
|
MIT
|
|
@@ -7,5 +7,9 @@ export declare const isDynamic: (key: string) => boolean;
|
|
|
7
7
|
* Returns true if the key represents a catch-all or optional catch-all segment.
|
|
8
8
|
*/
|
|
9
9
|
export declare const isCatchAllOrOptional: (key: string) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Returns true if the key represents an optional catch-all segment.
|
|
12
|
+
*/
|
|
13
|
+
export declare const isOptionalCatchAll: (key: string) => boolean;
|
|
10
14
|
export declare const isHttpMethod: (value: string) => value is HttpMethodFuncKey;
|
|
11
|
-
export declare const deepMerge: <T extends object, U extends object>(target: T, source: U) => T;
|
|
15
|
+
export declare const deepMerge: <T extends object, U extends object>(target: T, source: U) => T & U;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CATCH_ALL_PREFIX as
|
|
1
|
+
import{CATCH_ALL_PREFIX as a,DYNAMIC_PREFIX as p,HTTP_METHOD_FUNC_KEYS as h,OPTIONAL_CATCH_ALL_PREFIX as i}from"rpc4next-shared";const g=t=>t.startsWith(p),y=t=>t.startsWith(a)||t.startsWith(i),A=t=>t.startsWith(i),u=new Set(h),T=t=>u.has(t),c=t=>typeof t=="object"&&t!==null&&!Array.isArray(t),d=(t,s)=>{const e={...t};for(const n in s)if(Object.hasOwn(s,n)){const r=t[n],o=s[n];c(r)&&c(o)?e[n]=d(r,o):e[n]=o}return e};export{d as deepMerge,y as isCatchAllOrOptional,g as isDynamic,T as isHttpMethod,A as isOptionalCatchAll};
|
package/dist/rpc/client/match.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{searchParamsToObject as
|
|
1
|
+
import{searchParamsToObject as P}from"../lib/search-params";import{isCatchAllOrOptional as x}from"./client-utils";function a(n){if(n!=null)try{return decodeURIComponent(n)}catch{return n}}const D=n=>n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),O=(n,u)=>{const c=n.map(t=>t.replace(/^\/+|\/+$/g,"")).filter(Boolean),h=c.length===0?"/":c.reduce((t,e)=>u.includes(e)?e.startsWith("_____")?`${t}(?:/(.*))?`:e.startsWith("___")?`${t}/([^/]+(?:/[^/]+)*)`:`${t}/([^/]+)`:`${t}/${D(a(e))}`,""),g=new RegExp(`^${h}(?:/)?$`);return t=>{let e;try{e=new URL(t,"http://dummy")}catch{return null}const p=e.pathname,l=g.exec(p);if(!l)return null;const i={};for(let s=0;s<u.length;s++){const f=u[s],o=f.replace(/^_+/,""),r=l[s+1];if(x(f))if(r==null||r==="")i[o]=void 0;else{const R=r.split("/").filter(Boolean).map(d=>a(d)).filter(d=>d!==void 0);i[o]=R}else i[o]=a(r)}const m=P(e.searchParams),$=e.hash?e.hash.slice(1):void 0,_=a($);return{params:i,query:m,hash:_}}};export{O as matchPath};
|
package/dist/rpc/client/rpc.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Inspired by the design of Hono (https://github.com/honojs/hono)
|
|
3
3
|
* and pathpida (https://github.com/aspida/pathpida)
|
|
4
4
|
* particularly their routing structures and developer experience.
|
|
5
|
-
*/import{isDynamic as g}from"./client-utils";const
|
|
5
|
+
*/import{isDynamic as g,isOptionalCatchAll as w}from"./client-utils";const a=(r,e,n,l,o)=>{const p=function(){};return new Proxy(p,{apply(c,u,t){const i=t[0],s=n[n.length-1],f=o[o.length-1];if(!s||!g(s))throw new Error(`Cannot apply a value: "${s??""}" is not a dynamic segment.`);if(i===void 0&&!w(s)){const d=f??s;throw new Error(`Missing value for dynamic parameter: ${String(d)}`)}return a(r,e,n,{...l,[f]:i},o)},get(c,u){if(u==="then"||typeof u=="symbol")return;const t=String(u),i=r(t,{paths:n,params:l,dynamicKeys:o,options:e});return i!==void 0?i:g(t)?a(r,e,[...n,t],l,[...o,t]):a(r,e,[...n,t],l,o)}})},x=r=>(e="/",n={})=>a(r,n,[e],{},[]);export{x as makeCreateRpc};
|
package/dist/rpc/client/url.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const $=t=>{if(!t)return"";const
|
|
1
|
+
const $=t=>{if(!t)return"";const r=t.query?`?${new URLSearchParams(t.query).toString()}`:"",c=t.hash?`#${t.hash}`:"";return r+c},f=(t,r)=>t.replace(/\/_{5}(\w+)/g,r.optionalCatchAll).replace(/\/_{3}(\w+)/g,r.catchAll).replace(/\/_(\w+)/g,r.dynamic),l=t=>{try{return decodeURIComponent(t)}catch{return t}},p=t=>{const r=t.join("/");return(r?`/${r}`:"/").replace(/\/+/g,"/").replace(/\/+$/,"")||"/"},g=t=>t.startsWith("_____")?`[[...${t.slice(5)}]]`:t.startsWith("___")?`[...${t.slice(3)}]`:t.startsWith("_")?`[${t.slice(1)}]`:l(t),m=(t,r,c)=>{const s=t.shift(),h=p(t.map(n=>c.includes(n)?n:l(n))),u=c.reduce((n,e)=>{const a=r[e];return Array.isArray(a)?n.replace(`/${e}`,`/${a.map(encodeURIComponent).join("/")}`):a===void 0?n.replace(`/${e}`,""):n.replace(`/${e}`,`/${encodeURIComponent(a)}`)},h);return n=>{const e=`${u}${$(n)}`,a=p(t.map(g)),i={};for(const o in r){const d=o.replace(/^_+/,"");i[d]=r[o]}return{pathname:a,params:i,path:s?`${s.replace(/\/$/,"")}${e}`:e,relativePath:e}}};export{$ as buildUrlSuffix,m as createUrl,f as replaceDynamicSegments};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rpc4next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Inspired by Hono RPC and Pathpida, rpc4next brings a lightweight and intuitive RPC solution to Next.js, making server-client communication seamless",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"next.js",
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"author": "watanabe-1",
|
|
18
18
|
"type": "module",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20.19.2"
|
|
21
|
+
},
|
|
19
22
|
"module": "dist/index.js",
|
|
20
23
|
"types": "dist/index.d.ts",
|
|
21
24
|
"files": [
|
|
@@ -93,13 +96,13 @@
|
|
|
93
96
|
"test:watch": "vitest --watch",
|
|
94
97
|
"lint": "biome check src",
|
|
95
98
|
"lint:fix": "biome check --write src",
|
|
96
|
-
"typecheck": "tsc -
|
|
99
|
+
"typecheck": "tsc -p tsconfig.build.json --noEmit"
|
|
97
100
|
},
|
|
98
101
|
"dependencies": {
|
|
99
|
-
"rpc4next-shared": "^0.
|
|
102
|
+
"rpc4next-shared": "^0.3.0"
|
|
100
103
|
},
|
|
101
104
|
"peerDependencies": {
|
|
102
|
-
"next": "^14.0.0 || ^15.0.0"
|
|
105
|
+
"next": "^14.0.0 || ^15.0.0 || ^16.0.0"
|
|
103
106
|
},
|
|
104
107
|
"publishConfig": {
|
|
105
108
|
"access": "public"
|