@jokio/rpc 0.7.0 → 0.7.2

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
@@ -14,7 +14,8 @@ A type-safe RPC framework for TypeScript with Zod validation, designed for Expre
14
14
  - Runtime validation using Zod schemas
15
15
  - Express.js integration for server-side
16
16
  - Flexible fetch-based client with custom fetch support
17
- - Query parameters and request body validation
17
+ - Support for multiple HTTP methods (GET, POST, PUT, PATCH, DELETE, QUERY)
18
+ - Path parameters, query parameters, and request body validation
18
19
  - Automatic response validation
19
20
 
20
21
  ## Installation
@@ -34,6 +35,9 @@ import { z } from "zod"
34
35
  const routes = defineRoutes({
35
36
  GET: {
36
37
  "/user/:id": {
38
+ queryParams: z.object({
39
+ include: z.string().optional(),
40
+ }),
37
41
  response: z.object({
38
42
  id: z.string(),
39
43
  name: z.string(),
@@ -54,6 +58,19 @@ const routes = defineRoutes({
54
58
  }),
55
59
  },
56
60
  },
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
+ }),
72
+ },
73
+ },
57
74
  })
58
75
  ```
59
76
 
@@ -70,7 +87,9 @@ const router = express.Router()
70
87
 
71
88
  registerExpressRoutes(router, routes, {
72
89
  GET: {
73
- "/user/:id": async ({ params }) => {
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
74
93
  return {
75
94
  id: params.id,
76
95
  name: "John Doe",
@@ -80,6 +99,7 @@ registerExpressRoutes(router, routes, {
80
99
  },
81
100
  POST: {
82
101
  "/user": async ({ body }) => {
102
+ // body is validated by Zod
83
103
  return {
84
104
  id: "2",
85
105
  name: body.name,
@@ -87,6 +107,16 @@ registerExpressRoutes(router, routes, {
87
107
  }
88
108
  },
89
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
+ },
119
+ },
90
120
  })
91
121
 
92
122
  app.use("/api", router)
@@ -103,13 +133,28 @@ const client = createClient(routes, {
103
133
  validate: true, // Optional: validate requests on client-side
104
134
  })
105
135
 
106
- // Fully typed API calls
107
- const user = await client.GET("/users/23")
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
+ })
108
141
 
109
- const newUser = await client.POST("/users", {
142
+ // POST request with body
143
+ const newUser = await client.POST("/user", {
110
144
  name: "Jane Doe",
111
145
  email: "jane@example.com",
112
146
  })
147
+
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
+ )
113
158
  ```
114
159
 
115
160
  ## API Reference
@@ -120,7 +165,13 @@ Helper function to define routes with type inference.
120
165
 
121
166
  **Parameters:**
122
167
 
123
- - `routes`: Route definitions object containing GET and POST route configurations
168
+ - `routes`: Route definitions object containing method configurations (GET, POST, PUT, PATCH, DELETE, QUERY)
169
+
170
+ **Route Configuration:**
171
+
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
124
175
 
125
176
  ### `registerExpressRoutes(router, routes, handlers)`
126
177
 
@@ -130,10 +181,14 @@ Registers route handlers to an Express router with automatic validation.
130
181
 
131
182
  - `router`: Express Router instance
132
183
  - `routes`: Route definitions object
133
- - `handlers`: Handler functions for each route with optional context factory
184
+ - `handlers`: Handler functions for each route with optional configuration
134
185
  - `ctx`: Optional function `(req: Request) => TContext` to provide context to handlers
135
- - `GET`: Handler functions that receive `(data, ctx)` parameters
136
- - `POST`: Handler functions that receive `(data, ctx)` parameters
186
+ - `validation`: Optional boolean to enable response validation (default: false)
187
+ - `schemaFile`: Optional path to expose route schemas at `/__routes` endpoint
188
+ - `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `QUERY`: Handler functions that receive `(data, ctx)` parameters
189
+ - `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)
137
192
 
138
193
  ### `createClient(routes, options)`
139
194
 
@@ -146,7 +201,21 @@ Creates a type-safe HTTP client.
146
201
  - `baseUrl`: Base URL for API requests
147
202
  - `getHeaders`: Optional function that returns headers (sync or async)
148
203
  - `fetch`: Optional custom fetch function (useful for Node.js or testing)
149
- - `validate`: Enable client-side request validation (default: false)
204
+ - `validate`: Enable client-side request validation (default: true)
205
+ - `debug`: Enable debug logging (default: false)
206
+
207
+ **Client Methods:**
208
+
209
+ Each HTTP method has a type-safe method on the client:
210
+
211
+ - `GET(path, options?)`: For GET requests
212
+ - `options.params`: Path parameters
213
+ - `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)
150
219
 
151
220
  ## Type Safety
152
221
 
@@ -154,13 +223,24 @@ The library provides end-to-end type safety:
154
223
 
155
224
  ```typescript
156
225
  // TypeScript knows the exact shape of requests and responses
157
- const result = await client.POST("/users", {
226
+ const result = await client.POST("/user", {
158
227
  name: "John",
159
228
  email: "invalid-email", // Zod will catch this at runtime
160
229
  })
161
230
 
162
231
  // result is typed as { id: string; name: string; email: string }
163
232
  console.log(result.id)
233
+
234
+ // Path parameters are type-safe
235
+ const user = await client.GET("/user/:id", {
236
+ params: { id: "123" }, // TypeScript enforces correct parameter names
237
+ })
238
+
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
243
+ })
164
244
  ```
165
245
 
166
246
  ## Error Handling
@@ -169,13 +249,23 @@ The library throws errors for:
169
249
 
170
250
  - HTTP errors (non-2xx responses)
171
251
  - Validation errors (invalid request/response data)
252
+ - Missing path parameters
172
253
 
173
254
  ```typescript
174
255
  try {
175
- await client.POST("/users", invalidData)
256
+ await client.POST("/user", invalidData)
176
257
  } catch (error) {
177
258
  // Handle validation or HTTP errors
178
259
  }
260
+
261
+ // Missing path parameters will throw an error
262
+ try {
263
+ await client.GET("/user/:id", {
264
+ params: {}, // Missing 'id' parameter
265
+ })
266
+ } catch (error) {
267
+ // Error: Missing required parameter: "id" for path "/user/:id"
268
+ }
179
269
  ```
180
270
 
181
271
  ## License
package/dist/index.d.mts CHANGED
@@ -55,7 +55,7 @@ type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {
55
55
  };
56
56
  declare const registerExpressRoutes: <T extends Partial<RouterConfig>, TContext>(router: Router, routes: T, handlers: RouteHandlers<T, TContext> & {
57
57
  ctx?: (req: Request) => TContext;
58
- schemaFilePath?: string;
58
+ schemaFile?: string;
59
59
  validation?: boolean;
60
60
  }) => Router;
61
61
 
package/dist/index.d.ts CHANGED
@@ -55,7 +55,7 @@ type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {
55
55
  };
56
56
  declare const registerExpressRoutes: <T extends Partial<RouterConfig>, TContext>(router: Router, routes: T, handlers: RouteHandlers<T, TContext> & {
57
57
  ctx?: (req: Request) => TContext;
58
- schemaFilePath?: string;
58
+ schemaFile?: string;
59
59
  validation?: boolean;
60
60
  }) => Router;
61
61
 
package/dist/index.js CHANGED
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- var promises = require('fs/promises');
4
-
5
3
  // src/client.ts
6
4
  var replacePathParams = (path, params) => {
7
5
  const paramNames = /* @__PURE__ */ new Set();
@@ -91,7 +89,9 @@ var createClient = (routes, options) => {
91
89
  }
92
90
  return client;
93
91
  };
94
- var createRouteHandler = (method, routes, handlers, route) => {
92
+
93
+ // src/server.ts
94
+ var createRouteHandler = (method, routes, handlers, route, validation) => {
95
95
  return async (req, res, next) => {
96
96
  try {
97
97
  if (method === "QUERY" && req.method !== "QUERY") {
@@ -108,16 +108,14 @@ var createRouteHandler = (method, routes, handlers, route) => {
108
108
  }
109
109
  };
110
110
  const result = await handlers[method][route]?.(data, ctx);
111
- res.json(
112
- handlers.validation ? routeConfig?.response.parse(result) : result
113
- );
111
+ res.json(validation ? routeConfig?.response.parse(result) : result);
114
112
  } catch (err) {
115
113
  next(err);
116
114
  }
117
115
  };
118
116
  };
119
117
  var registerExpressRoutes = (router, routes, handlers) => {
120
- const { schemaFilePath } = handlers;
118
+ const { schemaFile, validation = true } = handlers;
121
119
  const expressMethodMap = {
122
120
  GET: "get",
123
121
  POST: "post",
@@ -133,15 +131,15 @@ var registerExpressRoutes = (router, routes, handlers) => {
133
131
  router = Object.keys(methodRoutes).reduce(
134
132
  (r, route) => r[routerMethod](
135
133
  route,
136
- createRouteHandler(methodKey, routes, handlers, route)
134
+ createRouteHandler(methodKey, routes, handlers, route, validation)
137
135
  ),
138
136
  router
139
137
  );
140
138
  }
141
- if (schemaFilePath) {
139
+ if (schemaFile) {
142
140
  router = router.get(
143
- "/__schema",
144
- async (_, res) => res.contentType("text/plain").send(await promises.readFile(schemaFilePath, "utf8"))
141
+ "/__routes",
142
+ async (_, res) => res.contentType("text/plain").send(schemaFile)
145
143
  );
146
144
  }
147
145
  return router;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/server.ts","../src/types.ts"],"names":["options","readFile"],"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 = true,\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 { readFile } from \"node:fs/promises\"\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 handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n validation?: boolean\n },\n route: string\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 = (handlers.ctx?.(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(\n handlers.validation ? routeConfig?.response.parse(result) : result\n )\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 handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n schemaFilePath?: string\n validation?: boolean\n }\n) => {\n const { schemaFilePath } = handlers\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(methodKey, routes, handlers, route)\n ),\n router\n )\n }\n\n if (schemaFilePath) {\n router = router.get(\"/__schema\", async (_, res) =>\n res\n .contentType(\"text/plain\")\n .send(await readFile(schemaFilePath!, \"utf8\"))\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<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 = true,\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 handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n },\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 = (handlers.ctx?.(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 handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n schemaFile?: string\n validation?: boolean\n }\n) => {\n const { schemaFile, validation = true } = handlers\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(methodKey, routes, handlers, route, validation)\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"]}
package/dist/index.mjs CHANGED
@@ -1,5 +1,3 @@
1
- import { readFile } from 'fs/promises';
2
-
3
1
  // src/client.ts
4
2
  var replacePathParams = (path, params) => {
5
3
  const paramNames = /* @__PURE__ */ new Set();
@@ -89,7 +87,9 @@ var createClient = (routes, options) => {
89
87
  }
90
88
  return client;
91
89
  };
92
- var createRouteHandler = (method, routes, handlers, route) => {
90
+
91
+ // src/server.ts
92
+ var createRouteHandler = (method, routes, handlers, route, validation) => {
93
93
  return async (req, res, next) => {
94
94
  try {
95
95
  if (method === "QUERY" && req.method !== "QUERY") {
@@ -106,16 +106,14 @@ var createRouteHandler = (method, routes, handlers, route) => {
106
106
  }
107
107
  };
108
108
  const result = await handlers[method][route]?.(data, ctx);
109
- res.json(
110
- handlers.validation ? routeConfig?.response.parse(result) : result
111
- );
109
+ res.json(validation ? routeConfig?.response.parse(result) : result);
112
110
  } catch (err) {
113
111
  next(err);
114
112
  }
115
113
  };
116
114
  };
117
115
  var registerExpressRoutes = (router, routes, handlers) => {
118
- const { schemaFilePath } = handlers;
116
+ const { schemaFile, validation = true } = handlers;
119
117
  const expressMethodMap = {
120
118
  GET: "get",
121
119
  POST: "post",
@@ -131,15 +129,15 @@ var registerExpressRoutes = (router, routes, handlers) => {
131
129
  router = Object.keys(methodRoutes).reduce(
132
130
  (r, route) => r[routerMethod](
133
131
  route,
134
- createRouteHandler(methodKey, routes, handlers, route)
132
+ createRouteHandler(methodKey, routes, handlers, route, validation)
135
133
  ),
136
134
  router
137
135
  );
138
136
  }
139
- if (schemaFilePath) {
137
+ if (schemaFile) {
140
138
  router = router.get(
141
- "/__schema",
142
- async (_, res) => res.contentType("text/plain").send(await readFile(schemaFilePath, "utf8"))
139
+ "/__routes",
140
+ async (_, res) => res.contentType("text/plain").send(schemaFile)
143
141
  );
144
142
  }
145
143
  return router;
@@ -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 = true,\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 { readFile } from \"node:fs/promises\"\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 handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n validation?: boolean\n },\n route: string\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 = (handlers.ctx?.(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(\n handlers.validation ? routeConfig?.response.parse(result) : result\n )\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 handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n schemaFilePath?: string\n validation?: boolean\n }\n) => {\n const { schemaFilePath } = handlers\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(methodKey, routes, handlers, route)\n ),\n router\n )\n }\n\n if (schemaFilePath) {\n router = router.get(\"/__schema\", async (_, res) =>\n res\n .contentType(\"text/plain\")\n .send(await readFile(schemaFilePath!, \"utf8\"))\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<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 = true,\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 handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n },\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 = (handlers.ctx?.(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 handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n schemaFile?: string\n validation?: boolean\n }\n) => {\n const { schemaFile, validation = true } = handlers\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(methodKey, routes, handlers, route, validation)\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jokio/rpc",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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",