@jokio/rpc 0.8.0 → 1.0.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
@@ -1,6 +1,6 @@
1
1
  # @jokio/rpc
2
2
 
3
- A type-safe RPC framework for TypeScript with Zod validation, designed for Express servers and HTTP clients.
3
+ A type-safe RPC framework for TypeScript designed for Express servers and HTTP clients. Supports both Zod schemas (with runtime validation) and plain TypeScript types (for type safety without runtime overhead).
4
4
 
5
5
  <img width="400" height="400" alt="ChatGPT Image Jan 4, 2026 at 10_15_01 AM" src="https://github.com/user-attachments/assets/5ca6462a-4d3a-46d6-ac09-31ecbc4d06fb" />
6
6
 
@@ -11,71 +11,77 @@ A type-safe RPC framework for TypeScript with Zod validation, designed for Expre
11
11
  ## Features
12
12
 
13
13
  - Full TypeScript type safety from server to client
14
- - Runtime validation using Zod schemas
14
+ - Two route definition styles: **Zod schemas** (with runtime validation) or **plain TypeScript types** (type-only, no runtime overhead)
15
15
  - Express.js integration for server-side
16
16
  - Flexible fetch-based client with custom fetch support
17
17
  - Support for multiple HTTP methods (GET, POST, PUT, PATCH, DELETE, QUERY)
18
- - Path parameters, query parameters, and request body validation
18
+ - Path parameters, query parameters, and request payload validation
19
19
  - Automatic response validation
20
20
 
21
21
  ## Installation
22
22
 
23
23
  ```bash
24
- npm install @jokio/rpc zod
24
+ npm install @jokio/rpc
25
+
26
+ # Optional: install zod if you want runtime validation
27
+ npm install zod
25
28
  ```
26
29
 
27
30
  ## Usage
28
31
 
29
32
  ### 1. Define Your Routes
30
33
 
34
+ You can define routes using **Zod schemas** (enables runtime validation) or **plain TypeScript types** (type safety only, no runtime cost).
35
+
36
+ #### Option A: Zod Schemas
37
+
31
38
  ```typescript
32
39
  import { defineRoutes } from "@jokio/rpc"
33
40
  import { z } from "zod"
34
41
 
35
42
  const routes = defineRoutes({
36
43
  GET: {
37
- "/user/:id": {
38
- queryParams: z.object({
39
- include: z.string().optional(),
40
- }),
41
- response: z.object({
42
- id: z.string(),
43
- name: z.string(),
44
- email: z.string(),
45
- }),
44
+ "/room/:id": {
45
+ response: z.object({ name: z.string() }),
46
46
  },
47
- },
48
- POST: {
49
- "/user": {
50
- body: z.object({
51
- name: z.string(),
52
- email: z.string().email(),
53
- }),
54
- response: z.object({
55
- id: z.string(),
56
- name: z.string(),
57
- email: z.string(),
58
- }),
47
+ "/rooms": {
48
+ response: z.any(),
59
49
  },
60
50
  },
61
- PUT: {
62
- "/user/:id": {
63
- body: z.object({
64
- name: z.string().optional(),
65
- email: z.string().email().optional(),
66
- }),
67
- response: z.object({
68
- id: z.string(),
69
- name: z.string(),
70
- email: z.string(),
71
- }),
51
+ POST: {
52
+ "/room": {
53
+ payload: z.any(),
54
+ response: z.any(),
72
55
  },
73
56
  },
74
57
  })
75
58
  ```
76
59
 
60
+ #### Option B: Plain TypeScript Types
61
+
62
+ ```typescript
63
+ type ApiRoutes = {
64
+ GET: {
65
+ "/room/:id": {
66
+ response: { name: string }
67
+ }
68
+ "/rooms": {
69
+ response: { count: number }
70
+ }
71
+ }
72
+ POST: {
73
+ "/room": {
74
+ payload: { name: string }
75
+ response: number
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
77
81
  ### 2. Set Up the Server
78
82
 
83
+ #### With Zod Routes
84
+
79
85
  ```typescript
80
86
  import express from "express"
81
87
  import { registerExpressRoutes } from "@jokio/rpc"
@@ -85,37 +91,13 @@ app.use(express.json())
85
91
 
86
92
  const router = express.Router()
87
93
 
88
- registerExpressRoutes(router, routes, {
94
+ registerExpressRoutes(router, { routes }, {
89
95
  GET: {
90
- "/user/:id": async ({ params, queryParams }) => {
91
- // params.id is type-safe and contains the :id from the path
92
- // queryParams.include is validated by Zod
93
- return {
94
- id: params.id,
95
- name: "John Doe",
96
- email: "john@example.com",
97
- }
98
- },
96
+ "/room/:id": ({ params }) => ({ name: params.id }),
97
+ "/rooms": () => ({ count: 10 }),
99
98
  },
100
99
  POST: {
101
- "/user": async ({ body }) => {
102
- // body is validated by Zod
103
- return {
104
- id: "2",
105
- name: body.name,
106
- email: body.email,
107
- }
108
- },
109
- },
110
- PUT: {
111
- "/user/:id": async ({ params, body }) => {
112
- // Both params and body are type-safe
113
- return {
114
- id: params.id,
115
- name: body.name ?? "John Doe",
116
- email: body.email ?? "john@example.com",
117
- }
118
- },
100
+ "/room": ({ payload }) => ({ id: "1" }),
119
101
  },
120
102
  })
121
103
 
@@ -123,38 +105,38 @@ app.use("/api", router)
123
105
  app.listen(3000)
124
106
  ```
125
107
 
126
- ### 3. Create a Type-Safe Client
108
+ #### With TypeScript Types
109
+
110
+ When using plain TypeScript types, pass the type as a generic parameter. No `routes` object is needed — you get full type safety without runtime validation.
127
111
 
128
112
  ```typescript
129
- import { createClient } from "@jokio/rpc"
113
+ registerExpressRoutes<ApiRoutes, { userId: number }>(
114
+ router,
115
+ { ctx: (req) => ({ userId: 123 }) },
116
+ {
117
+ GET: {
118
+ "/room/:id": ({ params }) => ({ name: params.id }),
119
+ "/rooms": (_, ctx) => ({ count: ctx.userId }),
120
+ },
121
+ POST: {
122
+ "/room": ({ payload }) => payload.name.length,
123
+ },
124
+ },
125
+ )
126
+ ```
130
127
 
131
- const client = createClient(routes, {
132
- baseUrl: "http://localhost:3000/api",
133
- validate: true, // Optional: validate requests on client-side
134
- })
128
+ ### 3. Create a Type-Safe Client
135
129
 
136
- // Fully typed API calls with path parameters
137
- const user = await client.GET("/user/:id", {
138
- params: { id: "23" },
139
- queryParams: { include: "profile" },
140
- })
130
+ The client uses the Zod route definitions for both type inference and optional runtime validation.
141
131
 
142
- // POST request with body
143
- const newUser = await client.POST("/user", {
144
- name: "Jane Doe",
145
- email: "jane@example.com",
146
- })
132
+ ```typescript
133
+ import { createClient } from "@jokio/rpc"
147
134
 
148
- // PUT request with path parameters and body
149
- const updatedUser = await client.PUT(
150
- "/user/:id",
151
- {
152
- name: "Jane Smith",
153
- },
154
- {
155
- params: { id: "23" },
156
- }
157
- )
135
+ const client = createClient("http://localhost:3000/api", { routes })
136
+
137
+ // Fully typed response — .name is inferred from the Zod schema
138
+ const room = await client.GET("/room/:id")
139
+ console.log(room.name)
158
140
  ```
159
141
 
160
142
  ## API Reference
@@ -169,39 +151,44 @@ Helper function to define routes with type inference.
169
151
 
170
152
  **Route Configuration:**
171
153
 
172
- - `body`: Zod schema for request body (not available for GET)
173
- - `queryParams`: Zod schema for query parameters (optional)
174
- - `response`: Zod schema for response data
154
+ Each route accepts the following fields as either a Zod schema or a plain TypeScript type:
175
155
 
176
- ### `registerExpressRoutes(router, routes, handlers)`
156
+ - `payload`: Request body (not available for GET)
157
+ - `queryParams`: Query parameters (optional)
158
+ - `response`: Response data
159
+
160
+ ### `registerExpressRoutes(router, config, handlers)`
177
161
 
178
162
  Registers route handlers to an Express router with automatic validation.
179
163
 
180
164
  **Parameters:**
181
165
 
182
166
  - `router`: Express Router instance
183
- - `routes`: Route definitions object
184
- - `handlers`: Handler functions for each route with optional configuration
167
+ - `config`: Configuration object
168
+ - `routes`: Optional route definitions object (Zod schemas omit when using plain TS types)
185
169
  - `ctx`: Optional function `(req: Request) => TContext` to provide context to handlers
186
- - `validation`: Optional boolean to enable response validation (default: false)
187
- - `schemaFile`: Optional path to expose route schemas at `/__routes` endpoint
170
+ - `validation`: Optional boolean to enable response validation (default: true)
171
+ - `schemaFile`: Optional string to expose route schemas at `/__routes` endpoint
172
+ - `handlers`: Handler functions for each route
188
173
  - `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `QUERY`: Handler functions that receive `(data, ctx)` parameters
189
174
  - `data.params`: Path parameters (e.g., `:id` in `/user/:id`)
190
- - `data.body`: Request body (validated by Zod)
191
- - `data.queryParams`: Query parameters (validated by Zod)
175
+ - `data.payload`: Request payload (validated by Zod if schemas provided)
176
+ - `data.queryParams`: Query parameters (validated by Zod if schemas provided)
177
+
178
+ When using plain TypeScript types, pass the type as a generic: `registerExpressRoutes<MyRoutes>(...)`. Zod validation is skipped since there are no schemas.
192
179
 
193
- ### `createClient(routes, options)`
180
+ ### `createClient(baseUrl, options)`
194
181
 
195
182
  Creates a type-safe HTTP client.
196
183
 
197
184
  **Parameters:**
198
185
 
199
- - `routes`: Route definitions object (same as used on the server)
186
+ - `baseUrl`: Base URL for API requests
200
187
  - `options`: Client configuration options
201
- - `baseUrl`: Base URL for API requests
188
+ - `routes`: Route definitions object (Zod schemas for type inference)
202
189
  - `getHeaders`: Optional function that returns headers (sync or async)
203
190
  - `fetch`: Optional custom fetch function (useful for Node.js or testing)
204
- - `validate`: Enable client-side request validation (default: true)
191
+ - `validate`: Enable client-side request validation (default: false)
205
192
  - `debug`: Enable debug logging (default: false)
206
193
 
207
194
  **Client Methods:**
@@ -211,35 +198,30 @@ Each HTTP method has a type-safe method on the client:
211
198
  - `GET(path, options?)`: For GET requests
212
199
  - `options.params`: Path parameters
213
200
  - `options.queryParams`: Query parameters
214
- - `POST(path, body, options?)`: For POST requests
215
- - `PUT(path, body, options?)`: For PUT requests
216
- - `PATCH(path, body, options?)`: For PATCH requests
217
- - `DELETE(path, body, options?)`: For DELETE requests
218
- - `QUERY(path, body, options?)`: For QUERY requests (custom method)
201
+ - `POST(path, payload, options?)`: For POST requests
202
+ - `PUT(path, payload, options?)`: For PUT requests
203
+ - `PATCH(path, payload, options?)`: For PATCH requests
204
+ - `DELETE(path, payload, options?)`: For DELETE requests
205
+ - `QUERY(path, payload, options?)`: For QUERY requests (custom method)
219
206
 
220
207
  ## Type Safety
221
208
 
222
- The library provides end-to-end type safety:
223
-
224
- ```typescript
225
- // TypeScript knows the exact shape of requests and responses
226
- const result = await client.POST("/user", {
227
- name: "John",
228
- email: "invalid-email", // Zod will catch this at runtime
229
- })
209
+ The library provides end-to-end type safety with both approaches:
230
210
 
231
- // result is typed as { id: string; name: string; email: string }
232
- console.log(result.id)
211
+ - **Zod schemas**: Types are inferred from schemas + runtime validation is available
212
+ - **Plain TypeScript types**: Types are enforced at compile time with zero runtime overhead
233
213
 
234
- // Path parameters are type-safe
235
- const user = await client.GET("/user/:id", {
236
- params: { id: "123" }, // TypeScript enforces correct parameter names
237
- })
214
+ ```typescript
215
+ // With Zod types are inferred, runtime validation available
216
+ const client = createClient("http://localhost:3000/api", { routes })
217
+ const room = await client.GET("/room/:id")
218
+ room.name // string — inferred from z.object({ name: z.string() })
238
219
 
239
- // Query parameters are validated
240
- const users = await client.GET("/user/:id", {
241
- params: { id: "123" },
242
- queryParams: { include: "profile" }, // Must match Zod schema
220
+ // With plain TS types — same type safety, no runtime cost
221
+ registerExpressRoutes<ApiRoutes>(router, {}, {
222
+ POST: {
223
+ "/room": ({ payload }) => payload.name.length, // payload typed as { name: string }
224
+ },
243
225
  })
244
226
  ```
245
227
 
package/dist/index.d.mts CHANGED
@@ -2,7 +2,7 @@ import z from 'zod';
2
2
  import { Router, Request } from 'express';
3
3
 
4
4
  type RouterConfig = {
5
- GET: Record<string, Omit<RouteConfig, "body">>;
5
+ GET: Record<string, Omit<RouteConfig, "payload">>;
6
6
  QUERY: Record<string, RouteConfig>;
7
7
  POST: Record<string, RouteConfig>;
8
8
  PUT: Record<string, RouteConfig>;
@@ -10,12 +10,12 @@ type RouterConfig = {
10
10
  DELETE: Record<string, RouteConfig>;
11
11
  };
12
12
  type RouteConfig = {
13
- body: z.ZodType;
14
- queryParams?: z.ZodType;
15
- response: z.ZodType;
13
+ payload: z.ZodType | unknown;
14
+ queryParams?: z.ZodType | unknown;
15
+ response: z.ZodType | unknown;
16
16
  };
17
- type InferRouteConfig<T extends RouteConfig | Omit<RouteConfig, "body">> = {
18
- [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never;
17
+ type InferRouteConfig<T extends RouteConfig | Omit<RouteConfig, "payload">> = {
18
+ [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : T[K];
19
19
  };
20
20
  declare const defineRoutes: <T extends Partial<RouterConfig>>(routes: T) => T;
21
21
  type ExtractRouteParams<T extends string> = T extends `${infer _Start}:${infer Param}/${infer Rest}` ? Rest extends `:${string}` ? {
@@ -32,17 +32,17 @@ type ClientOptions<TConfig, K> = Omit<TConfig, "response"> & {
32
32
  params?: K extends string ? ExtractRouteParams<K> : unknown;
33
33
  };
34
34
  type RouterClient<T extends Partial<RouterConfig>> = {
35
- [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any> ? M extends "GET" ? <K extends keyof T[M]>(path: K, options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, "body">, K>) => Promise<InferRouteConfig<T[M][K]>["response"]> : <K extends keyof T[M]>(path: K, body: InferRouteConfig<T[M][K]>["body"], options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, "body">, K>) => Promise<InferRouteConfig<T[M][K]>["response"]> : never;
35
+ [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any> ? M extends "GET" ? <K extends keyof T[M]>(path: K, options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, "payload">, K>) => Promise<InferRouteConfig<T[M][K]>["response"]> : <K extends keyof T[M]>(path: K, payload: InferRouteConfig<T[M][K]>["payload"], options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, "payload">, K>) => Promise<InferRouteConfig<T[M][K]>["response"]> : never;
36
36
  };
37
37
  type FetchFunction = (url: string, options: RequestInit) => Promise<Response>;
38
- type CreateClientOptions = {
39
- baseUrl: string;
38
+ type CreateClientOptions<T extends Partial<RouterConfig>> = {
39
+ routes?: T;
40
40
  getHeaders?: () => Promise<Record<string, string>> | Record<string, string>;
41
41
  fetch?: FetchFunction;
42
42
  validate?: boolean;
43
43
  debug?: boolean;
44
44
  };
45
- declare const createClient: <T extends Partial<RouterConfig>>(routes: T, options: CreateClientOptions) => RouterClient<T>;
45
+ declare const createClient: <T extends Partial<RouterConfig>>(baseUrl: string, options?: CreateClientOptions<T>) => RouterClient<T>;
46
46
 
47
47
  type MaybePromise<T> = Promise<T> | T;
48
48
  type HandlerData<TConfig, K> = Omit<TConfig, "response"> & {
@@ -50,10 +50,11 @@ type HandlerData<TConfig, K> = Omit<TConfig, "response"> & {
50
50
  };
51
51
  type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {
52
52
  [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any> ? {
53
- [K in keyof T[M]]: T[M][K] extends RouteConfig | Omit<RouteConfig, "body"> ? (data: M extends "GET" ? HandlerData<Omit<InferRouteConfig<T[M][K]>, "body">, K> : HandlerData<InferRouteConfig<T[M][K]>, K>, ctx: TContext) => MaybePromise<InferRouteConfig<T[M][K]>["response"]> : never;
53
+ [K in keyof T[M]]: T[M][K] extends RouteConfig | Omit<RouteConfig, "payload"> ? (data: M extends "GET" ? HandlerData<Omit<InferRouteConfig<T[M][K]>, "payload">, K> : HandlerData<InferRouteConfig<T[M][K]>, K>, ctx: TContext) => MaybePromise<InferRouteConfig<T[M][K]>["response"]> : never;
54
54
  } : never;
55
55
  };
56
- declare const registerExpressRoutes: <T extends Partial<RouterConfig>, TContext>(router: Router, routes: T, config: {
56
+ declare const registerExpressRoutes: <T extends Partial<RouterConfig>, TContext>(router: Router, config: {
57
+ routes?: T;
57
58
  ctx?: (req: Request) => TContext;
58
59
  schemaFile?: string;
59
60
  validation?: boolean;
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import z from 'zod';
2
2
  import { Router, Request } from 'express';
3
3
 
4
4
  type RouterConfig = {
5
- GET: Record<string, Omit<RouteConfig, "body">>;
5
+ GET: Record<string, Omit<RouteConfig, "payload">>;
6
6
  QUERY: Record<string, RouteConfig>;
7
7
  POST: Record<string, RouteConfig>;
8
8
  PUT: Record<string, RouteConfig>;
@@ -10,12 +10,12 @@ type RouterConfig = {
10
10
  DELETE: Record<string, RouteConfig>;
11
11
  };
12
12
  type RouteConfig = {
13
- body: z.ZodType;
14
- queryParams?: z.ZodType;
15
- response: z.ZodType;
13
+ payload: z.ZodType | unknown;
14
+ queryParams?: z.ZodType | unknown;
15
+ response: z.ZodType | unknown;
16
16
  };
17
- type InferRouteConfig<T extends RouteConfig | Omit<RouteConfig, "body">> = {
18
- [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never;
17
+ type InferRouteConfig<T extends RouteConfig | Omit<RouteConfig, "payload">> = {
18
+ [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : T[K];
19
19
  };
20
20
  declare const defineRoutes: <T extends Partial<RouterConfig>>(routes: T) => T;
21
21
  type ExtractRouteParams<T extends string> = T extends `${infer _Start}:${infer Param}/${infer Rest}` ? Rest extends `:${string}` ? {
@@ -32,17 +32,17 @@ type ClientOptions<TConfig, K> = Omit<TConfig, "response"> & {
32
32
  params?: K extends string ? ExtractRouteParams<K> : unknown;
33
33
  };
34
34
  type RouterClient<T extends Partial<RouterConfig>> = {
35
- [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any> ? M extends "GET" ? <K extends keyof T[M]>(path: K, options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, "body">, K>) => Promise<InferRouteConfig<T[M][K]>["response"]> : <K extends keyof T[M]>(path: K, body: InferRouteConfig<T[M][K]>["body"], options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, "body">, K>) => Promise<InferRouteConfig<T[M][K]>["response"]> : never;
35
+ [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any> ? M extends "GET" ? <K extends keyof T[M]>(path: K, options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, "payload">, K>) => Promise<InferRouteConfig<T[M][K]>["response"]> : <K extends keyof T[M]>(path: K, payload: InferRouteConfig<T[M][K]>["payload"], options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, "payload">, K>) => Promise<InferRouteConfig<T[M][K]>["response"]> : never;
36
36
  };
37
37
  type FetchFunction = (url: string, options: RequestInit) => Promise<Response>;
38
- type CreateClientOptions = {
39
- baseUrl: string;
38
+ type CreateClientOptions<T extends Partial<RouterConfig>> = {
39
+ routes?: T;
40
40
  getHeaders?: () => Promise<Record<string, string>> | Record<string, string>;
41
41
  fetch?: FetchFunction;
42
42
  validate?: boolean;
43
43
  debug?: boolean;
44
44
  };
45
- declare const createClient: <T extends Partial<RouterConfig>>(routes: T, options: CreateClientOptions) => RouterClient<T>;
45
+ declare const createClient: <T extends Partial<RouterConfig>>(baseUrl: string, options?: CreateClientOptions<T>) => RouterClient<T>;
46
46
 
47
47
  type MaybePromise<T> = Promise<T> | T;
48
48
  type HandlerData<TConfig, K> = Omit<TConfig, "response"> & {
@@ -50,10 +50,11 @@ type HandlerData<TConfig, K> = Omit<TConfig, "response"> & {
50
50
  };
51
51
  type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {
52
52
  [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any> ? {
53
- [K in keyof T[M]]: T[M][K] extends RouteConfig | Omit<RouteConfig, "body"> ? (data: M extends "GET" ? HandlerData<Omit<InferRouteConfig<T[M][K]>, "body">, K> : HandlerData<InferRouteConfig<T[M][K]>, K>, ctx: TContext) => MaybePromise<InferRouteConfig<T[M][K]>["response"]> : never;
53
+ [K in keyof T[M]]: T[M][K] extends RouteConfig | Omit<RouteConfig, "payload"> ? (data: M extends "GET" ? HandlerData<Omit<InferRouteConfig<T[M][K]>, "payload">, K> : HandlerData<InferRouteConfig<T[M][K]>, K>, ctx: TContext) => MaybePromise<InferRouteConfig<T[M][K]>["response"]> : never;
54
54
  } : never;
55
55
  };
56
- declare const registerExpressRoutes: <T extends Partial<RouterConfig>, TContext>(router: Router, routes: T, config: {
56
+ declare const registerExpressRoutes: <T extends Partial<RouterConfig>, TContext>(router: Router, config: {
57
+ routes?: T;
57
58
  ctx?: (req: Request) => TContext;
58
59
  schemaFile?: string;
59
60
  validation?: boolean;
package/dist/index.js CHANGED
@@ -19,23 +19,23 @@ var replacePathParams = (path, params) => {
19
19
  return String(params[paramName]);
20
20
  });
21
21
  };
22
- var createClient = (routes, options) => {
22
+ var createClient = (baseUrl, options) => {
23
23
  const {
24
- baseUrl,
24
+ routes,
25
25
  getHeaders = () => Promise.resolve({}),
26
26
  fetch: customFetch = fetch,
27
27
  validate = false
28
- } = options;
28
+ } = options ?? {};
29
29
  const buildUrl = (path, options2) => {
30
30
  const queryString = options2?.queryParams ? "?" + new URLSearchParams(options2.queryParams).toString() : "";
31
31
  const finalPath = path.includes(":") ? replacePathParams(path, options2?.params ?? {}) : path;
32
32
  return `${baseUrl}${finalPath}${queryString}`;
33
33
  };
34
- const handleValidation = (method, path, body, options2) => {
34
+ const handleValidation = (method, path, payload, options2) => {
35
35
  if (!validate) return;
36
- const routeConfig = routes[method]?.[path];
37
- if (body && routeConfig?.body) {
38
- routeConfig.body.parse(body);
36
+ const routeConfig = routes?.[method]?.[path];
37
+ if (payload && routeConfig?.payload) {
38
+ routeConfig.payload.parse(payload);
39
39
  }
40
40
  if (options2?.queryParams && routeConfig?.queryParams) {
41
41
  routeConfig.queryParams.parse(options2.queryParams);
@@ -49,7 +49,7 @@ var createClient = (routes, options) => {
49
49
  }
50
50
  throw new Error(error.message);
51
51
  }
52
- const routeConfig = routes[method]?.[path];
52
+ const routeConfig = routes?.[method]?.[path];
53
53
  if (routeConfig?.response?.type === "void") {
54
54
  await response.text();
55
55
  return;
@@ -57,8 +57,8 @@ var createClient = (routes, options) => {
57
57
  const json = await response.json();
58
58
  return validate && routeConfig?.response ? routeConfig.response.parse(json) : json;
59
59
  };
60
- const makeRequest = async (method, path, body, options2) => {
61
- handleValidation(method, path, body, options2);
60
+ const makeRequest = async (method, path, payload, options2) => {
61
+ handleValidation(method, path, payload, options2);
62
62
  const url = buildUrl(path, options2);
63
63
  const fetchOptions = {
64
64
  method,
@@ -67,26 +67,21 @@ var createClient = (routes, options) => {
67
67
  ...await getHeaders()
68
68
  }
69
69
  };
70
- if (body !== void 0) {
71
- fetchOptions.body = JSON.stringify(body);
70
+ if (payload !== void 0) {
71
+ fetchOptions.body = JSON.stringify(payload);
72
72
  }
73
73
  const response = await customFetch(url, fetchOptions);
74
74
  return handleResponse(method, path, response, options2);
75
75
  };
76
- const client = {};
77
76
  const methodHandlers = {
78
77
  GET: async (path, options2) => makeRequest("GET", path, void 0, options2),
79
- QUERY: async (path, body, options2) => makeRequest("QUERY", path, body, options2),
80
- POST: async (path, body, options2) => makeRequest("POST", path, body, options2),
81
- PUT: async (path, body, options2) => makeRequest("PUT", path, body, options2),
82
- PATCH: async (path, body, options2) => makeRequest("PATCH", path, body, options2),
83
- DELETE: async (path, body, options2) => makeRequest("DELETE", path, body, options2)
78
+ QUERY: async (path, payload, options2) => makeRequest("QUERY", path, payload, options2),
79
+ POST: async (path, payload, options2) => makeRequest("POST", path, payload, options2),
80
+ PUT: async (path, payload, options2) => makeRequest("PUT", path, payload, options2),
81
+ PATCH: async (path, payload, options2) => makeRequest("PATCH", path, payload, options2),
82
+ DELETE: async (path, payload, options2) => makeRequest("DELETE", path, payload, options2)
84
83
  };
85
- for (const method of Object.keys(routes)) {
86
- if (method in methodHandlers) {
87
- client[method] = methodHandlers[method];
88
- }
89
- }
84
+ const client = methodHandlers;
90
85
  return client;
91
86
  };
92
87
 
@@ -98,24 +93,39 @@ var createRouteHandler = (method, routes, getCtx, handlers, route, validation) =
98
93
  res.status(405).send("Method Not Allowed");
99
94
  return;
100
95
  }
96
+ const validationCheck = {
97
+ payload: typeof validation === "boolean" ? validation : validation.payload ?? false,
98
+ queryParams: typeof validation === "boolean" ? validation : validation.queryParams ?? false,
99
+ response: typeof validation === "boolean" ? validation : validation.response ?? false
100
+ };
101
101
  const ctx = getCtx(req) ?? {};
102
- const routeConfig = routes[method][route];
102
+ const routeConfig = (routes?.[method])[route];
103
103
  const data = {
104
104
  params: req.params,
105
- ...routeConfig?.body && { body: routeConfig.body.parse(req.body) },
106
- ...routeConfig?.queryParams && {
105
+ ...routeConfig?.payload && validationCheck.payload && {
106
+ payload: routeConfig.payload.parse(req.body)
107
+ },
108
+ ...routeConfig?.queryParams && validationCheck.queryParams && {
107
109
  queryParams: routeConfig.queryParams.parse(req.query)
108
110
  }
109
111
  };
110
112
  const result = await handlers[method][route]?.(data, ctx);
111
- res.json(validation ? routeConfig?.response.parse(result) : result);
113
+ res.json(
114
+ routeConfig?.response && validationCheck.response ? routeConfig?.response.parse(result) : result
115
+ );
112
116
  } catch (err) {
117
+ console.warn(method, route, err?.message);
113
118
  next(err);
114
119
  }
115
120
  };
116
121
  };
117
- var registerExpressRoutes = (router, routes, config, handlers) => {
118
- const { schemaFile, validation = true, ctx = () => null } = config;
122
+ var registerExpressRoutes = (router, config, handlers) => {
123
+ const {
124
+ schemaFile,
125
+ validation = true,
126
+ ctx = () => null,
127
+ routes
128
+ } = config;
119
129
  const expressMethodMap = {
120
130
  GET: "get",
121
131
  POST: "post",
@@ -126,7 +136,7 @@ var registerExpressRoutes = (router, routes, config, handlers) => {
126
136
  };
127
137
  for (const [method, routerMethod] of Object.entries(expressMethodMap)) {
128
138
  const methodKey = method;
129
- const methodRoutes = routes[methodKey];
139
+ const methodRoutes = handlers[methodKey];
130
140
  if (!methodRoutes) continue;
131
141
  router = Object.keys(methodRoutes).reduce(
132
142
  (r, route) => r[routerMethod](
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/server.ts","../src/types.ts"],"names":["options"],"mappings":"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","file":"index.js","sourcesContent":["import {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for client options with optional params\ntype ClientOptions<TConfig, K> = Omit<TConfig, \"response\"> & {\n params?: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouterClient<T extends Partial<RouterConfig>> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? M extends \"GET\"\n ? <K extends keyof T[M]>(\n path: K,\n options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : <K extends keyof T[M]>(\n path: K,\n body: InferRouteConfig<T[M][K]>[\"body\"],\n options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n}\n\ntype FetchFunction = (url: string, options: RequestInit) => Promise<Response>\n\ntype CreateClientOptions = {\n baseUrl: string\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n fetch?: FetchFunction\n validate?: boolean\n debug?: boolean\n}\n\n/**\n * Replaces path parameters with their values.\n * @param path - The path template with parameters (e.g., \"/:id/test/:name/info\")\n * @param params - The parameter values (e.g., {id: \"123\", name: \"434\"})\n * @returns The resolved path (e.g., \"/123/test/434/info\")\n * @throws Error if a required parameter is missing\n */\nexport const replacePathParams = (\n path: string,\n params: Record<string, string | number>\n): string => {\n const paramNames = new Set<string>()\n const paramPattern = /:([^/]+)/g\n let match: RegExpExecArray | null\n\n // Extract all parameter names from the path\n while ((match = paramPattern.exec(path)) !== null) {\n paramNames.add(match[1])\n }\n\n // Check if all required parameters are provided\n for (const paramName of paramNames) {\n if (!(paramName in params)) {\n throw new Error(\n `Missing required parameter: \"${paramName}\" for path \"${path}\"`\n )\n }\n }\n\n // Replace all parameters with their values\n return path.replace(/:([^/]+)/g, (_, paramName) => {\n return String(params[paramName])\n })\n}\n\nexport const createClient = <T extends Partial<RouterConfig>>(\n routes: T,\n options: CreateClientOptions\n): RouterClient<T> => {\n const {\n baseUrl,\n getHeaders = () => Promise.resolve({}),\n fetch: customFetch = fetch,\n validate = false,\n } = options\n\n const buildUrl = (path: string, options?: any): string => {\n const queryString = options?.queryParams\n ? \"?\" + new URLSearchParams(options.queryParams).toString()\n : \"\"\n\n const finalPath = path.includes(\":\")\n ? replacePathParams(path, options?.params ?? {})\n : path\n\n return `${baseUrl}${finalPath}${queryString}`\n }\n\n const handleValidation = (\n method: keyof T & keyof RouterConfig,\n path: string,\n body?: any,\n options?: any\n ) => {\n if (!validate) return\n\n const routeConfig = (routes[method] as any)?.[path]\n if (body && routeConfig?.body) {\n routeConfig.body.parse(body)\n }\n if (options?.queryParams && routeConfig?.queryParams) {\n routeConfig.queryParams.parse(options.queryParams)\n }\n }\n\n const handleResponse = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n response: Response,\n options?: any\n ) => {\n if (!response.ok) {\n const error: any = await response.json()\n\n if (options?.debug) {\n console.debug(error)\n }\n\n throw new Error(error.message)\n }\n\n const routeConfig = (routes[method] as any)?.[path]\n if (routeConfig?.response?.type === \"void\") {\n await response.text()\n return\n }\n\n const json = await response.json()\n\n return validate && routeConfig?.response\n ? routeConfig.response.parse(json)\n : json\n }\n\n const makeRequest = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n body?: any,\n options?: any\n ) => {\n handleValidation(method, path, body, options)\n\n const url = buildUrl(path, options)\n const fetchOptions: RequestInit = {\n method: method as string,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n }\n\n if (body !== undefined) {\n fetchOptions.body = JSON.stringify(body)\n }\n\n const response = await customFetch(url, fetchOptions)\n\n return handleResponse(method, path, response, options)\n }\n\n const client = {} as RouterClient<T>\n\n const methodHandlers = {\n GET: async (path: any, options?: any) =>\n makeRequest(\"GET\", path, undefined, options),\n QUERY: async (path: any, body: any, options?: any) =>\n makeRequest(\"QUERY\", path, body, options),\n POST: async (path: any, body: any, options?: any) =>\n makeRequest(\"POST\", path, body, options),\n PUT: async (path: any, body: any, options?: any) =>\n makeRequest(\"PUT\", path, body, options),\n PATCH: async (path: any, body: any, options?: any) =>\n makeRequest(\"PATCH\", path, body, options),\n DELETE: async (path: any, body: any, options?: any) =>\n makeRequest(\"DELETE\", path, body, options),\n }\n\n for (const method of Object.keys(routes) as Array<\n keyof T & keyof RouterConfig\n >) {\n if (method in methodHandlers) {\n ;(client as any)[method] = methodHandlers[method]\n }\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for sync or async responses\ntype MaybePromise<T> = Promise<T> | T\n\n// Reusable type for handler data with params\ntype HandlerData<TConfig, K> = Omit<TConfig, \"response\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? {\n [K in keyof T[M]]: T[M][K] extends\n | RouteConfig\n | Omit<RouteConfig, \"body\">\n ? (\n data: M extends \"GET\"\n ? HandlerData<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n : HandlerData<InferRouteConfig<T[M][K]>, K>,\n ctx: TContext\n ) => MaybePromise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n }\n : never\n}\n\nconst createRouteHandler = <\n T extends Partial<RouterConfig>,\n TContext,\n M extends keyof RouteHandlers<T, TContext>\n>(\n method: M,\n routes: T,\n getCtx: (req: Request) => TContext,\n handlers: RouteHandlers<T, TContext> & {},\n route: string,\n validation: boolean\n) => {\n return async (req: Request, res: any, next: any) => {\n try {\n if (method === \"QUERY\" && req.method !== \"QUERY\") {\n res.status(405).send(\"Method Not Allowed\")\n return\n }\n\n const ctx = (getCtx(req) ?? {}) as TContext\n const routeConfig = (routes[method] as any)[route]\n\n const data = {\n params: req.params,\n ...(routeConfig?.body && { body: routeConfig.body.parse(req.body) }),\n ...(routeConfig?.queryParams && {\n queryParams: routeConfig.queryParams.parse(req.query),\n }),\n }\n\n const result = await handlers[method][route]?.(data as any, ctx)\n\n res.json(validation ? routeConfig?.response.parse(result) : result)\n } catch (err) {\n next(err)\n }\n }\n}\n\nexport const registerExpressRoutes = <\n T extends Partial<RouterConfig>,\n TContext\n>(\n router: Router,\n routes: T,\n config: {\n ctx?: (req: Request) => TContext\n schemaFile?: string\n validation?: boolean\n },\n handlers: RouteHandlers<T, TContext>\n) => {\n const { schemaFile, validation = true, ctx = () => null as TContext } = config\n\n const expressMethodMap = {\n GET: \"get\",\n POST: \"post\",\n PUT: \"put\",\n PATCH: \"patch\",\n DELETE: \"delete\",\n QUERY: \"all\",\n } as const\n\n for (const [method, routerMethod] of Object.entries(expressMethodMap)) {\n const methodKey = method as keyof RouteHandlers<T, TContext>\n const methodRoutes = routes[methodKey]\n\n if (!methodRoutes) continue\n\n router = Object.keys(methodRoutes as object).reduce(\n (r, route) =>\n r[routerMethod](\n route,\n createRouteHandler(\n methodKey,\n routes,\n ctx,\n handlers,\n route,\n validation\n )\n ),\n router\n )\n }\n\n if (schemaFile) {\n router = router.get(\"/__routes\", async (_, res) =>\n res.contentType(\"text/plain\").send(schemaFile)\n )\n }\n\n return router\n}\n","import type z from \"zod\"\n\nexport type RouterConfig = {\n GET: Record<string, Omit<RouteConfig, \"body\">>\n QUERY: Record<string, RouteConfig>\n POST: Record<string, RouteConfig>\n PUT: Record<string, RouteConfig>\n PATCH: Record<string, RouteConfig>\n DELETE: Record<string, RouteConfig>\n}\n\nexport type RouteConfig = {\n body: z.ZodType\n queryParams?: z.ZodType\n response: z.ZodType\n}\n\nexport type InferRouteConfig<\n T extends RouteConfig | Omit<RouteConfig, \"body\">\n> = {\n [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never\n}\n\nexport const defineRoutes = <T extends Partial<RouterConfig>>(\n routes: T\n): T => routes\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/info\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\nexport type ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Rest extends `:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : Rest extends `${string}/:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : { [K in Param]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/server.ts","../src/types.ts"],"names":["options"],"mappings":"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","file":"index.js","sourcesContent":["import {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for client options with optional params\ntype ClientOptions<TConfig, K> = Omit<TConfig, \"response\"> & {\n params?: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouterClient<T extends Partial<RouterConfig>> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? M extends \"GET\"\n ? <K extends keyof T[M]>(\n path: K,\n options?: ClientOptions<\n Omit<InferRouteConfig<T[M][K]>, \"payload\">,\n K\n >,\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : <K extends keyof T[M]>(\n path: K,\n payload: InferRouteConfig<T[M][K]>[\"payload\"],\n options?: ClientOptions<\n Omit<InferRouteConfig<T[M][K]>, \"payload\">,\n K\n >,\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n}\n\ntype FetchFunction = (url: string, options: RequestInit) => Promise<Response>\n\ntype CreateClientOptions<T extends Partial<RouterConfig>> = {\n routes?: T\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n fetch?: FetchFunction\n validate?: boolean\n debug?: boolean\n}\n\n/**\n * Replaces path parameters with their values.\n * @param path - The path template with parameters (e.g., \"/:id/test/:name/info\")\n * @param params - The parameter values (e.g., {id: \"123\", name: \"434\"})\n * @returns The resolved path (e.g., \"/123/test/434/info\")\n * @throws Error if a required parameter is missing\n */\nexport const replacePathParams = (\n path: string,\n params: Record<string, string | number>,\n): string => {\n const paramNames = new Set<string>()\n const paramPattern = /:([^/]+)/g\n let match: RegExpExecArray | null\n\n // Extract all parameter names from the path\n while ((match = paramPattern.exec(path)) !== null) {\n paramNames.add(match[1])\n }\n\n // Check if all required parameters are provided\n for (const paramName of paramNames) {\n if (!(paramName in params)) {\n throw new Error(\n `Missing required parameter: \"${paramName}\" for path \"${path}\"`,\n )\n }\n }\n\n // Replace all parameters with their values\n return path.replace(/:([^/]+)/g, (_, paramName) => {\n return String(params[paramName])\n })\n}\n\nexport const createClient = <T extends Partial<RouterConfig>>(\n baseUrl: string,\n options?: CreateClientOptions<T>,\n): RouterClient<T> => {\n const {\n routes,\n getHeaders = () => Promise.resolve({}),\n fetch: customFetch = fetch,\n validate = false,\n } = options ?? {}\n\n const buildUrl = (path: string, options?: any): string => {\n const queryString = options?.queryParams\n ? \"?\" + new URLSearchParams(options.queryParams).toString()\n : \"\"\n\n const finalPath = path.includes(\":\")\n ? replacePathParams(path, options?.params ?? {})\n : path\n\n return `${baseUrl}${finalPath}${queryString}`\n }\n\n const handleValidation = (\n method: keyof T & keyof RouterConfig,\n path: string,\n payload?: any,\n options?: any,\n ) => {\n if (!validate) return\n\n const routeConfig = (routes?.[method] as any)?.[path]\n if (payload && routeConfig?.payload) {\n routeConfig.payload.parse(payload)\n }\n if (options?.queryParams && routeConfig?.queryParams) {\n routeConfig.queryParams.parse(options.queryParams)\n }\n }\n\n const handleResponse = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n response: Response,\n options?: any,\n ) => {\n if (!response.ok) {\n const error: any = await response.json()\n\n if (options?.debug) {\n console.debug(error)\n }\n\n throw new Error(error.message)\n }\n\n const routeConfig = (routes?.[method] as any)?.[path]\n if (routeConfig?.response?.type === \"void\") {\n await response.text()\n return\n }\n\n const json = await response.json()\n\n return validate && routeConfig?.response\n ? routeConfig.response.parse(json)\n : json\n }\n\n const makeRequest = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n payload?: any,\n options?: any,\n ) => {\n handleValidation(method, path, payload, options)\n\n const url = buildUrl(path, options)\n const fetchOptions: RequestInit = {\n method: method as string,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n }\n\n if (payload !== undefined) {\n fetchOptions.body = JSON.stringify(payload)\n }\n\n const response = await customFetch(url, fetchOptions)\n\n return handleResponse(method, path, response, options)\n }\n\n const methodHandlers = {\n GET: async (path: any, options?: any) =>\n makeRequest(\"GET\", path, undefined, options),\n QUERY: async (path: any, payload: any, options?: any) =>\n makeRequest(\"QUERY\", path, payload, options),\n POST: async (path: any, payload: any, options?: any) =>\n makeRequest(\"POST\", path, payload, options),\n PUT: async (path: any, payload: any, options?: any) =>\n makeRequest(\"PUT\", path, payload, options),\n PATCH: async (path: any, payload: any, options?: any) =>\n makeRequest(\"PATCH\", path, payload, options),\n DELETE: async (path: any, payload: any, options?: any) =>\n makeRequest(\"DELETE\", path, payload, options),\n }\n\n const client = methodHandlers as RouterClient<T>\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for sync or async responses\ntype MaybePromise<T> = Promise<T> | T\n\n// Reusable type for handler data with params\ntype HandlerData<TConfig, K> = Omit<TConfig, \"response\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? {\n [K in keyof T[M]]: T[M][K] extends\n | RouteConfig\n | Omit<RouteConfig, \"payload\">\n ? (\n data: M extends \"GET\"\n ? HandlerData<Omit<InferRouteConfig<T[M][K]>, \"payload\">, K>\n : HandlerData<InferRouteConfig<T[M][K]>, K>,\n ctx: TContext,\n ) => MaybePromise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n }\n : never\n}\n\nconst createRouteHandler = <\n T extends Partial<RouterConfig>,\n TContext,\n M extends keyof RouteHandlers<T, TContext>,\n>(\n method: M,\n routes: T | undefined,\n getCtx: (req: Request) => TContext,\n handlers: RouteHandlers<T, TContext> & {},\n route: string,\n validation:\n | boolean\n | {\n payload?: boolean\n queryParams?: boolean\n response?: boolean\n },\n) => {\n return async (req: Request, res: any, next: any) => {\n try {\n if (method === \"QUERY\" && req.method !== \"QUERY\") {\n res.status(405).send(\"Method Not Allowed\")\n return\n }\n\n const validationCheck = {\n payload:\n typeof validation === \"boolean\"\n ? validation\n : (validation.payload ?? false),\n\n queryParams:\n typeof validation === \"boolean\"\n ? validation\n : (validation.queryParams ?? false),\n\n response:\n typeof validation === \"boolean\"\n ? validation\n : (validation.response ?? false),\n }\n\n const ctx = (getCtx(req) ?? {}) as TContext\n const routeConfig = (routes?.[method] as any)[route]\n\n const data = {\n params: req.params,\n\n ...(routeConfig?.payload &&\n validationCheck.payload && {\n payload: routeConfig.payload.parse(req.body),\n }),\n\n ...(routeConfig?.queryParams &&\n validationCheck.queryParams && {\n queryParams: routeConfig.queryParams.parse(req.query),\n }),\n }\n\n const result = await handlers[method][route]?.(data as any, ctx)\n\n res.json(\n routeConfig?.response && validationCheck.response\n ? routeConfig?.response.parse(result)\n : result,\n )\n } catch (err: any) {\n console.warn(method, route, err?.message)\n next(err)\n }\n }\n}\n\nexport const registerExpressRoutes = <\n T extends Partial<RouterConfig>,\n TContext,\n>(\n router: Router,\n config: {\n routes?: T\n ctx?: (req: Request) => TContext\n schemaFile?: string\n validation?: boolean\n },\n handlers: RouteHandlers<T, TContext>,\n) => {\n const {\n schemaFile,\n validation = true,\n ctx = () => null as TContext,\n routes,\n } = config\n\n const expressMethodMap = {\n GET: \"get\",\n POST: \"post\",\n PUT: \"put\",\n PATCH: \"patch\",\n DELETE: \"delete\",\n QUERY: \"all\",\n } as const\n\n for (const [method, routerMethod] of Object.entries(expressMethodMap)) {\n const methodKey = method as keyof RouteHandlers<T, TContext>\n const methodRoutes = handlers[methodKey]\n\n if (!methodRoutes) continue\n\n router = Object.keys(methodRoutes as object).reduce(\n (r, route) =>\n r[routerMethod](\n route,\n createRouteHandler(\n methodKey,\n routes,\n ctx,\n handlers,\n route,\n validation,\n ),\n ),\n router,\n )\n }\n\n if (schemaFile) {\n router = router.get(\"/__routes\", async (_, res) =>\n res.contentType(\"text/plain\").send(schemaFile),\n )\n }\n\n return router\n}\n","import type z from \"zod\"\n\nexport type RouterConfig = {\n GET: Record<string, Omit<RouteConfig, \"payload\">>\n QUERY: Record<string, RouteConfig>\n POST: Record<string, RouteConfig>\n PUT: Record<string, RouteConfig>\n PATCH: Record<string, RouteConfig>\n DELETE: Record<string, RouteConfig>\n}\n\nexport type RouteConfig = {\n payload: z.ZodType | unknown\n queryParams?: z.ZodType | unknown\n response: z.ZodType | unknown\n}\n\nexport type InferRouteConfig<\n T extends RouteConfig | Omit<RouteConfig, \"payload\">,\n> = {\n [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : T[K]\n}\n\nexport const defineRoutes = <T extends Partial<RouterConfig>>(routes: T): T =>\n routes\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/info\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\nexport type ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Rest extends `:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : Rest extends `${string}/:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : { [K in Param]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n"]}
package/dist/index.mjs CHANGED
@@ -17,23 +17,23 @@ var replacePathParams = (path, params) => {
17
17
  return String(params[paramName]);
18
18
  });
19
19
  };
20
- var createClient = (routes, options) => {
20
+ var createClient = (baseUrl, options) => {
21
21
  const {
22
- baseUrl,
22
+ routes,
23
23
  getHeaders = () => Promise.resolve({}),
24
24
  fetch: customFetch = fetch,
25
25
  validate = false
26
- } = options;
26
+ } = options ?? {};
27
27
  const buildUrl = (path, options2) => {
28
28
  const queryString = options2?.queryParams ? "?" + new URLSearchParams(options2.queryParams).toString() : "";
29
29
  const finalPath = path.includes(":") ? replacePathParams(path, options2?.params ?? {}) : path;
30
30
  return `${baseUrl}${finalPath}${queryString}`;
31
31
  };
32
- const handleValidation = (method, path, body, options2) => {
32
+ const handleValidation = (method, path, payload, options2) => {
33
33
  if (!validate) return;
34
- const routeConfig = routes[method]?.[path];
35
- if (body && routeConfig?.body) {
36
- routeConfig.body.parse(body);
34
+ const routeConfig = routes?.[method]?.[path];
35
+ if (payload && routeConfig?.payload) {
36
+ routeConfig.payload.parse(payload);
37
37
  }
38
38
  if (options2?.queryParams && routeConfig?.queryParams) {
39
39
  routeConfig.queryParams.parse(options2.queryParams);
@@ -47,7 +47,7 @@ var createClient = (routes, options) => {
47
47
  }
48
48
  throw new Error(error.message);
49
49
  }
50
- const routeConfig = routes[method]?.[path];
50
+ const routeConfig = routes?.[method]?.[path];
51
51
  if (routeConfig?.response?.type === "void") {
52
52
  await response.text();
53
53
  return;
@@ -55,8 +55,8 @@ var createClient = (routes, options) => {
55
55
  const json = await response.json();
56
56
  return validate && routeConfig?.response ? routeConfig.response.parse(json) : json;
57
57
  };
58
- const makeRequest = async (method, path, body, options2) => {
59
- handleValidation(method, path, body, options2);
58
+ const makeRequest = async (method, path, payload, options2) => {
59
+ handleValidation(method, path, payload, options2);
60
60
  const url = buildUrl(path, options2);
61
61
  const fetchOptions = {
62
62
  method,
@@ -65,26 +65,21 @@ var createClient = (routes, options) => {
65
65
  ...await getHeaders()
66
66
  }
67
67
  };
68
- if (body !== void 0) {
69
- fetchOptions.body = JSON.stringify(body);
68
+ if (payload !== void 0) {
69
+ fetchOptions.body = JSON.stringify(payload);
70
70
  }
71
71
  const response = await customFetch(url, fetchOptions);
72
72
  return handleResponse(method, path, response, options2);
73
73
  };
74
- const client = {};
75
74
  const methodHandlers = {
76
75
  GET: async (path, options2) => makeRequest("GET", path, void 0, options2),
77
- QUERY: async (path, body, options2) => makeRequest("QUERY", path, body, options2),
78
- POST: async (path, body, options2) => makeRequest("POST", path, body, options2),
79
- PUT: async (path, body, options2) => makeRequest("PUT", path, body, options2),
80
- PATCH: async (path, body, options2) => makeRequest("PATCH", path, body, options2),
81
- DELETE: async (path, body, options2) => makeRequest("DELETE", path, body, options2)
76
+ QUERY: async (path, payload, options2) => makeRequest("QUERY", path, payload, options2),
77
+ POST: async (path, payload, options2) => makeRequest("POST", path, payload, options2),
78
+ PUT: async (path, payload, options2) => makeRequest("PUT", path, payload, options2),
79
+ PATCH: async (path, payload, options2) => makeRequest("PATCH", path, payload, options2),
80
+ DELETE: async (path, payload, options2) => makeRequest("DELETE", path, payload, options2)
82
81
  };
83
- for (const method of Object.keys(routes)) {
84
- if (method in methodHandlers) {
85
- client[method] = methodHandlers[method];
86
- }
87
- }
82
+ const client = methodHandlers;
88
83
  return client;
89
84
  };
90
85
 
@@ -96,24 +91,39 @@ var createRouteHandler = (method, routes, getCtx, handlers, route, validation) =
96
91
  res.status(405).send("Method Not Allowed");
97
92
  return;
98
93
  }
94
+ const validationCheck = {
95
+ payload: typeof validation === "boolean" ? validation : validation.payload ?? false,
96
+ queryParams: typeof validation === "boolean" ? validation : validation.queryParams ?? false,
97
+ response: typeof validation === "boolean" ? validation : validation.response ?? false
98
+ };
99
99
  const ctx = getCtx(req) ?? {};
100
- const routeConfig = routes[method][route];
100
+ const routeConfig = (routes?.[method])[route];
101
101
  const data = {
102
102
  params: req.params,
103
- ...routeConfig?.body && { body: routeConfig.body.parse(req.body) },
104
- ...routeConfig?.queryParams && {
103
+ ...routeConfig?.payload && validationCheck.payload && {
104
+ payload: routeConfig.payload.parse(req.body)
105
+ },
106
+ ...routeConfig?.queryParams && validationCheck.queryParams && {
105
107
  queryParams: routeConfig.queryParams.parse(req.query)
106
108
  }
107
109
  };
108
110
  const result = await handlers[method][route]?.(data, ctx);
109
- res.json(validation ? routeConfig?.response.parse(result) : result);
111
+ res.json(
112
+ routeConfig?.response && validationCheck.response ? routeConfig?.response.parse(result) : result
113
+ );
110
114
  } catch (err) {
115
+ console.warn(method, route, err?.message);
111
116
  next(err);
112
117
  }
113
118
  };
114
119
  };
115
- var registerExpressRoutes = (router, routes, config, handlers) => {
116
- const { schemaFile, validation = true, ctx = () => null } = config;
120
+ var registerExpressRoutes = (router, config, handlers) => {
121
+ const {
122
+ schemaFile,
123
+ validation = true,
124
+ ctx = () => null,
125
+ routes
126
+ } = config;
117
127
  const expressMethodMap = {
118
128
  GET: "get",
119
129
  POST: "post",
@@ -124,7 +134,7 @@ var registerExpressRoutes = (router, routes, config, handlers) => {
124
134
  };
125
135
  for (const [method, routerMethod] of Object.entries(expressMethodMap)) {
126
136
  const methodKey = method;
127
- const methodRoutes = routes[methodKey];
137
+ const methodRoutes = handlers[methodKey];
128
138
  if (!methodRoutes) continue;
129
139
  router = Object.keys(methodRoutes).reduce(
130
140
  (r, route) => r[routerMethod](
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/server.ts","../src/types.ts"],"names":["options"],"mappings":"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","file":"index.mjs","sourcesContent":["import {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for client options with optional params\ntype ClientOptions<TConfig, K> = Omit<TConfig, \"response\"> & {\n params?: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouterClient<T extends Partial<RouterConfig>> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? M extends \"GET\"\n ? <K extends keyof T[M]>(\n path: K,\n options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : <K extends keyof T[M]>(\n path: K,\n body: InferRouteConfig<T[M][K]>[\"body\"],\n options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n}\n\ntype FetchFunction = (url: string, options: RequestInit) => Promise<Response>\n\ntype CreateClientOptions = {\n baseUrl: string\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n fetch?: FetchFunction\n validate?: boolean\n debug?: boolean\n}\n\n/**\n * Replaces path parameters with their values.\n * @param path - The path template with parameters (e.g., \"/:id/test/:name/info\")\n * @param params - The parameter values (e.g., {id: \"123\", name: \"434\"})\n * @returns The resolved path (e.g., \"/123/test/434/info\")\n * @throws Error if a required parameter is missing\n */\nexport const replacePathParams = (\n path: string,\n params: Record<string, string | number>\n): string => {\n const paramNames = new Set<string>()\n const paramPattern = /:([^/]+)/g\n let match: RegExpExecArray | null\n\n // Extract all parameter names from the path\n while ((match = paramPattern.exec(path)) !== null) {\n paramNames.add(match[1])\n }\n\n // Check if all required parameters are provided\n for (const paramName of paramNames) {\n if (!(paramName in params)) {\n throw new Error(\n `Missing required parameter: \"${paramName}\" for path \"${path}\"`\n )\n }\n }\n\n // Replace all parameters with their values\n return path.replace(/:([^/]+)/g, (_, paramName) => {\n return String(params[paramName])\n })\n}\n\nexport const createClient = <T extends Partial<RouterConfig>>(\n routes: T,\n options: CreateClientOptions\n): RouterClient<T> => {\n const {\n baseUrl,\n getHeaders = () => Promise.resolve({}),\n fetch: customFetch = fetch,\n validate = false,\n } = options\n\n const buildUrl = (path: string, options?: any): string => {\n const queryString = options?.queryParams\n ? \"?\" + new URLSearchParams(options.queryParams).toString()\n : \"\"\n\n const finalPath = path.includes(\":\")\n ? replacePathParams(path, options?.params ?? {})\n : path\n\n return `${baseUrl}${finalPath}${queryString}`\n }\n\n const handleValidation = (\n method: keyof T & keyof RouterConfig,\n path: string,\n body?: any,\n options?: any\n ) => {\n if (!validate) return\n\n const routeConfig = (routes[method] as any)?.[path]\n if (body && routeConfig?.body) {\n routeConfig.body.parse(body)\n }\n if (options?.queryParams && routeConfig?.queryParams) {\n routeConfig.queryParams.parse(options.queryParams)\n }\n }\n\n const handleResponse = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n response: Response,\n options?: any\n ) => {\n if (!response.ok) {\n const error: any = await response.json()\n\n if (options?.debug) {\n console.debug(error)\n }\n\n throw new Error(error.message)\n }\n\n const routeConfig = (routes[method] as any)?.[path]\n if (routeConfig?.response?.type === \"void\") {\n await response.text()\n return\n }\n\n const json = await response.json()\n\n return validate && routeConfig?.response\n ? routeConfig.response.parse(json)\n : json\n }\n\n const makeRequest = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n body?: any,\n options?: any\n ) => {\n handleValidation(method, path, body, options)\n\n const url = buildUrl(path, options)\n const fetchOptions: RequestInit = {\n method: method as string,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n }\n\n if (body !== undefined) {\n fetchOptions.body = JSON.stringify(body)\n }\n\n const response = await customFetch(url, fetchOptions)\n\n return handleResponse(method, path, response, options)\n }\n\n const client = {} as RouterClient<T>\n\n const methodHandlers = {\n GET: async (path: any, options?: any) =>\n makeRequest(\"GET\", path, undefined, options),\n QUERY: async (path: any, body: any, options?: any) =>\n makeRequest(\"QUERY\", path, body, options),\n POST: async (path: any, body: any, options?: any) =>\n makeRequest(\"POST\", path, body, options),\n PUT: async (path: any, body: any, options?: any) =>\n makeRequest(\"PUT\", path, body, options),\n PATCH: async (path: any, body: any, options?: any) =>\n makeRequest(\"PATCH\", path, body, options),\n DELETE: async (path: any, body: any, options?: any) =>\n makeRequest(\"DELETE\", path, body, options),\n }\n\n for (const method of Object.keys(routes) as Array<\n keyof T & keyof RouterConfig\n >) {\n if (method in methodHandlers) {\n ;(client as any)[method] = methodHandlers[method]\n }\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for sync or async responses\ntype MaybePromise<T> = Promise<T> | T\n\n// Reusable type for handler data with params\ntype HandlerData<TConfig, K> = Omit<TConfig, \"response\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? {\n [K in keyof T[M]]: T[M][K] extends\n | RouteConfig\n | Omit<RouteConfig, \"body\">\n ? (\n data: M extends \"GET\"\n ? HandlerData<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n : HandlerData<InferRouteConfig<T[M][K]>, K>,\n ctx: TContext\n ) => MaybePromise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n }\n : never\n}\n\nconst createRouteHandler = <\n T extends Partial<RouterConfig>,\n TContext,\n M extends keyof RouteHandlers<T, TContext>\n>(\n method: M,\n routes: T,\n getCtx: (req: Request) => TContext,\n handlers: RouteHandlers<T, TContext> & {},\n route: string,\n validation: boolean\n) => {\n return async (req: Request, res: any, next: any) => {\n try {\n if (method === \"QUERY\" && req.method !== \"QUERY\") {\n res.status(405).send(\"Method Not Allowed\")\n return\n }\n\n const ctx = (getCtx(req) ?? {}) as TContext\n const routeConfig = (routes[method] as any)[route]\n\n const data = {\n params: req.params,\n ...(routeConfig?.body && { body: routeConfig.body.parse(req.body) }),\n ...(routeConfig?.queryParams && {\n queryParams: routeConfig.queryParams.parse(req.query),\n }),\n }\n\n const result = await handlers[method][route]?.(data as any, ctx)\n\n res.json(validation ? routeConfig?.response.parse(result) : result)\n } catch (err) {\n next(err)\n }\n }\n}\n\nexport const registerExpressRoutes = <\n T extends Partial<RouterConfig>,\n TContext\n>(\n router: Router,\n routes: T,\n config: {\n ctx?: (req: Request) => TContext\n schemaFile?: string\n validation?: boolean\n },\n handlers: RouteHandlers<T, TContext>\n) => {\n const { schemaFile, validation = true, ctx = () => null as TContext } = config\n\n const expressMethodMap = {\n GET: \"get\",\n POST: \"post\",\n PUT: \"put\",\n PATCH: \"patch\",\n DELETE: \"delete\",\n QUERY: \"all\",\n } as const\n\n for (const [method, routerMethod] of Object.entries(expressMethodMap)) {\n const methodKey = method as keyof RouteHandlers<T, TContext>\n const methodRoutes = routes[methodKey]\n\n if (!methodRoutes) continue\n\n router = Object.keys(methodRoutes as object).reduce(\n (r, route) =>\n r[routerMethod](\n route,\n createRouteHandler(\n methodKey,\n routes,\n ctx,\n handlers,\n route,\n validation\n )\n ),\n router\n )\n }\n\n if (schemaFile) {\n router = router.get(\"/__routes\", async (_, res) =>\n res.contentType(\"text/plain\").send(schemaFile)\n )\n }\n\n return router\n}\n","import type z from \"zod\"\n\nexport type RouterConfig = {\n GET: Record<string, Omit<RouteConfig, \"body\">>\n QUERY: Record<string, RouteConfig>\n POST: Record<string, RouteConfig>\n PUT: Record<string, RouteConfig>\n PATCH: Record<string, RouteConfig>\n DELETE: Record<string, RouteConfig>\n}\n\nexport type RouteConfig = {\n body: z.ZodType\n queryParams?: z.ZodType\n response: z.ZodType\n}\n\nexport type InferRouteConfig<\n T extends RouteConfig | Omit<RouteConfig, \"body\">\n> = {\n [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never\n}\n\nexport const defineRoutes = <T extends Partial<RouterConfig>>(\n routes: T\n): T => routes\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/info\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\nexport type ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Rest extends `:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : Rest extends `${string}/:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : { [K in Param]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/server.ts","../src/types.ts"],"names":["options"],"mappings":"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","file":"index.mjs","sourcesContent":["import {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for client options with optional params\ntype ClientOptions<TConfig, K> = Omit<TConfig, \"response\"> & {\n params?: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouterClient<T extends Partial<RouterConfig>> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? M extends \"GET\"\n ? <K extends keyof T[M]>(\n path: K,\n options?: ClientOptions<\n Omit<InferRouteConfig<T[M][K]>, \"payload\">,\n K\n >,\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : <K extends keyof T[M]>(\n path: K,\n payload: InferRouteConfig<T[M][K]>[\"payload\"],\n options?: ClientOptions<\n Omit<InferRouteConfig<T[M][K]>, \"payload\">,\n K\n >,\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n}\n\ntype FetchFunction = (url: string, options: RequestInit) => Promise<Response>\n\ntype CreateClientOptions<T extends Partial<RouterConfig>> = {\n routes?: T\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n fetch?: FetchFunction\n validate?: boolean\n debug?: boolean\n}\n\n/**\n * Replaces path parameters with their values.\n * @param path - The path template with parameters (e.g., \"/:id/test/:name/info\")\n * @param params - The parameter values (e.g., {id: \"123\", name: \"434\"})\n * @returns The resolved path (e.g., \"/123/test/434/info\")\n * @throws Error if a required parameter is missing\n */\nexport const replacePathParams = (\n path: string,\n params: Record<string, string | number>,\n): string => {\n const paramNames = new Set<string>()\n const paramPattern = /:([^/]+)/g\n let match: RegExpExecArray | null\n\n // Extract all parameter names from the path\n while ((match = paramPattern.exec(path)) !== null) {\n paramNames.add(match[1])\n }\n\n // Check if all required parameters are provided\n for (const paramName of paramNames) {\n if (!(paramName in params)) {\n throw new Error(\n `Missing required parameter: \"${paramName}\" for path \"${path}\"`,\n )\n }\n }\n\n // Replace all parameters with their values\n return path.replace(/:([^/]+)/g, (_, paramName) => {\n return String(params[paramName])\n })\n}\n\nexport const createClient = <T extends Partial<RouterConfig>>(\n baseUrl: string,\n options?: CreateClientOptions<T>,\n): RouterClient<T> => {\n const {\n routes,\n getHeaders = () => Promise.resolve({}),\n fetch: customFetch = fetch,\n validate = false,\n } = options ?? {}\n\n const buildUrl = (path: string, options?: any): string => {\n const queryString = options?.queryParams\n ? \"?\" + new URLSearchParams(options.queryParams).toString()\n : \"\"\n\n const finalPath = path.includes(\":\")\n ? replacePathParams(path, options?.params ?? {})\n : path\n\n return `${baseUrl}${finalPath}${queryString}`\n }\n\n const handleValidation = (\n method: keyof T & keyof RouterConfig,\n path: string,\n payload?: any,\n options?: any,\n ) => {\n if (!validate) return\n\n const routeConfig = (routes?.[method] as any)?.[path]\n if (payload && routeConfig?.payload) {\n routeConfig.payload.parse(payload)\n }\n if (options?.queryParams && routeConfig?.queryParams) {\n routeConfig.queryParams.parse(options.queryParams)\n }\n }\n\n const handleResponse = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n response: Response,\n options?: any,\n ) => {\n if (!response.ok) {\n const error: any = await response.json()\n\n if (options?.debug) {\n console.debug(error)\n }\n\n throw new Error(error.message)\n }\n\n const routeConfig = (routes?.[method] as any)?.[path]\n if (routeConfig?.response?.type === \"void\") {\n await response.text()\n return\n }\n\n const json = await response.json()\n\n return validate && routeConfig?.response\n ? routeConfig.response.parse(json)\n : json\n }\n\n const makeRequest = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n payload?: any,\n options?: any,\n ) => {\n handleValidation(method, path, payload, options)\n\n const url = buildUrl(path, options)\n const fetchOptions: RequestInit = {\n method: method as string,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n }\n\n if (payload !== undefined) {\n fetchOptions.body = JSON.stringify(payload)\n }\n\n const response = await customFetch(url, fetchOptions)\n\n return handleResponse(method, path, response, options)\n }\n\n const methodHandlers = {\n GET: async (path: any, options?: any) =>\n makeRequest(\"GET\", path, undefined, options),\n QUERY: async (path: any, payload: any, options?: any) =>\n makeRequest(\"QUERY\", path, payload, options),\n POST: async (path: any, payload: any, options?: any) =>\n makeRequest(\"POST\", path, payload, options),\n PUT: async (path: any, payload: any, options?: any) =>\n makeRequest(\"PUT\", path, payload, options),\n PATCH: async (path: any, payload: any, options?: any) =>\n makeRequest(\"PATCH\", path, payload, options),\n DELETE: async (path: any, payload: any, options?: any) =>\n makeRequest(\"DELETE\", path, payload, options),\n }\n\n const client = methodHandlers as RouterClient<T>\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for sync or async responses\ntype MaybePromise<T> = Promise<T> | T\n\n// Reusable type for handler data with params\ntype HandlerData<TConfig, K> = Omit<TConfig, \"response\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? {\n [K in keyof T[M]]: T[M][K] extends\n | RouteConfig\n | Omit<RouteConfig, \"payload\">\n ? (\n data: M extends \"GET\"\n ? HandlerData<Omit<InferRouteConfig<T[M][K]>, \"payload\">, K>\n : HandlerData<InferRouteConfig<T[M][K]>, K>,\n ctx: TContext,\n ) => MaybePromise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n }\n : never\n}\n\nconst createRouteHandler = <\n T extends Partial<RouterConfig>,\n TContext,\n M extends keyof RouteHandlers<T, TContext>,\n>(\n method: M,\n routes: T | undefined,\n getCtx: (req: Request) => TContext,\n handlers: RouteHandlers<T, TContext> & {},\n route: string,\n validation:\n | boolean\n | {\n payload?: boolean\n queryParams?: boolean\n response?: boolean\n },\n) => {\n return async (req: Request, res: any, next: any) => {\n try {\n if (method === \"QUERY\" && req.method !== \"QUERY\") {\n res.status(405).send(\"Method Not Allowed\")\n return\n }\n\n const validationCheck = {\n payload:\n typeof validation === \"boolean\"\n ? validation\n : (validation.payload ?? false),\n\n queryParams:\n typeof validation === \"boolean\"\n ? validation\n : (validation.queryParams ?? false),\n\n response:\n typeof validation === \"boolean\"\n ? validation\n : (validation.response ?? false),\n }\n\n const ctx = (getCtx(req) ?? {}) as TContext\n const routeConfig = (routes?.[method] as any)[route]\n\n const data = {\n params: req.params,\n\n ...(routeConfig?.payload &&\n validationCheck.payload && {\n payload: routeConfig.payload.parse(req.body),\n }),\n\n ...(routeConfig?.queryParams &&\n validationCheck.queryParams && {\n queryParams: routeConfig.queryParams.parse(req.query),\n }),\n }\n\n const result = await handlers[method][route]?.(data as any, ctx)\n\n res.json(\n routeConfig?.response && validationCheck.response\n ? routeConfig?.response.parse(result)\n : result,\n )\n } catch (err: any) {\n console.warn(method, route, err?.message)\n next(err)\n }\n }\n}\n\nexport const registerExpressRoutes = <\n T extends Partial<RouterConfig>,\n TContext,\n>(\n router: Router,\n config: {\n routes?: T\n ctx?: (req: Request) => TContext\n schemaFile?: string\n validation?: boolean\n },\n handlers: RouteHandlers<T, TContext>,\n) => {\n const {\n schemaFile,\n validation = true,\n ctx = () => null as TContext,\n routes,\n } = config\n\n const expressMethodMap = {\n GET: \"get\",\n POST: \"post\",\n PUT: \"put\",\n PATCH: \"patch\",\n DELETE: \"delete\",\n QUERY: \"all\",\n } as const\n\n for (const [method, routerMethod] of Object.entries(expressMethodMap)) {\n const methodKey = method as keyof RouteHandlers<T, TContext>\n const methodRoutes = handlers[methodKey]\n\n if (!methodRoutes) continue\n\n router = Object.keys(methodRoutes as object).reduce(\n (r, route) =>\n r[routerMethod](\n route,\n createRouteHandler(\n methodKey,\n routes,\n ctx,\n handlers,\n route,\n validation,\n ),\n ),\n router,\n )\n }\n\n if (schemaFile) {\n router = router.get(\"/__routes\", async (_, res) =>\n res.contentType(\"text/plain\").send(schemaFile),\n )\n }\n\n return router\n}\n","import type z from \"zod\"\n\nexport type RouterConfig = {\n GET: Record<string, Omit<RouteConfig, \"payload\">>\n QUERY: Record<string, RouteConfig>\n POST: Record<string, RouteConfig>\n PUT: Record<string, RouteConfig>\n PATCH: Record<string, RouteConfig>\n DELETE: Record<string, RouteConfig>\n}\n\nexport type RouteConfig = {\n payload: z.ZodType | unknown\n queryParams?: z.ZodType | unknown\n response: z.ZodType | unknown\n}\n\nexport type InferRouteConfig<\n T extends RouteConfig | Omit<RouteConfig, \"payload\">,\n> = {\n [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : T[K]\n}\n\nexport const defineRoutes = <T extends Partial<RouterConfig>>(routes: T): T =>\n routes\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/info\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\nexport type ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Rest extends `:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : Rest extends `${string}/:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : { [K in Param]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jokio/rpc",
3
- "version": "0.8.0",
3
+ "version": "1.0.0",
4
4
  "description": "Type-safe RPC framework with Zod validation for Express and TypeScript",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",