@jokio/rpc 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  A type-safe RPC framework for TypeScript with Zod validation, designed for Express servers and HTTP clients.
4
4
 
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
+
7
+
5
8
  ## Features
6
9
 
7
10
  - Full TypeScript type safety from server to client
@@ -20,13 +23,13 @@ npm install @jokio/rpc
20
23
 
21
24
  ## Usage
22
25
 
23
- ### 1. Define Your Router Configuration
26
+ ### 1. Define Your Routes
24
27
 
25
28
  ```typescript
26
- import { defineRouterConfig } from "@jokio/rpc"
29
+ import { defineRoutes } from "@jokio/rpc"
27
30
  import { z } from "zod"
28
31
 
29
- const routerConfig = defineRouterConfig({
32
+ const routes = defineRoutes({
30
33
  GET: {
31
34
  "/users/:id": {
32
35
  query: z.object({
@@ -62,14 +65,14 @@ const routerConfig = defineRouterConfig({
62
65
 
63
66
  ```typescript
64
67
  import express from "express"
65
- import { applyConfigToExpressRouter } from "@jokio/rpc"
68
+ import { createExpressRouter } from "@jokio/rpc"
66
69
 
67
70
  const app = express()
68
71
  app.use(express.json())
69
72
 
70
73
  const router = express.Router()
71
74
 
72
- applyConfigToExpressRouter(router, routerConfig, {
75
+ createExpressRouter(router, routes, {
73
76
  // Optional: Define a context factory function
74
77
  ctx: (req) => ({
75
78
  userId: req.headers["x-user-id"] as string,
@@ -108,9 +111,9 @@ app.listen(3000)
108
111
  ```typescript
109
112
  import { createClient } from "@jokio/rpc"
110
113
 
111
- const client = createClient(routerConfig, {
114
+ const client = createClient(routes, {
112
115
  baseUrl: "http://localhost:3000/api",
113
- validateRequest: true, // Optional: validate requests on client-side
116
+ validate: true, // Optional: validate requests on client-side
114
117
  })
115
118
 
116
119
  // Fully typed API calls
@@ -127,33 +130,38 @@ const newUser = await client.POST(
127
130
 
128
131
  ## API Reference
129
132
 
130
- ### `defineRouterConfig(config)`
133
+ ### `defineRoutes(routes)`
131
134
 
132
- Helper function to define a router configuration with type inference.
135
+ Helper function to define routes with type inference.
133
136
 
134
- ### `applyConfigToExpressRouter(router, config, handlers)`
137
+ **Parameters:**
138
+ - `routes`: Route definitions object containing GET and POST route configurations
139
+
140
+ ### `createExpressRouter(router, routes, handlers)`
135
141
 
136
142
  Applies route handlers to an Express router with automatic validation.
137
143
 
138
144
  **Parameters:**
139
145
 
140
146
  - `router`: Express Router instance
141
- - `config`: Router configuration object
147
+ - `routes`: Route definitions object
142
148
  - `handlers`: Handler functions for each route with optional context factory
143
149
  - `ctx`: Optional function `(req: Request) => TContext` to provide context to handlers
144
150
  - `GET`: Handler functions that receive `(data, ctx)` parameters
145
151
  - `POST`: Handler functions that receive `(data, ctx)` parameters
146
152
 
147
- ### `createClient(config, options)`
153
+ ### `createClient(routes, options)`
148
154
 
149
155
  Creates a type-safe HTTP client.
150
156
 
151
- **Options:**
157
+ **Parameters:**
152
158
 
153
- - `baseUrl`: Base URL for API requests
154
- - `headers`: Optional default headers
155
- - `fetch`: Optional custom fetch function (useful for Node.js or testing)
156
- - `validateRequest`: Enable client-side request validation (default: false)
159
+ - `routes`: Route definitions object (same as used on the server)
160
+ - `options`: Client configuration options
161
+ - `baseUrl`: Base URL for API requests
162
+ - `getHeaders`: Optional function that returns headers (sync or async)
163
+ - `fetch`: Optional custom fetch function (useful for Node.js or testing)
164
+ - `validate`: Enable client-side request validation (default: false)
157
165
 
158
166
  ## Type Safety
159
167
 
package/dist/index.d.mts CHANGED
@@ -13,7 +13,7 @@ type RouterConfig = {
13
13
  type InferRouteConfig<T extends RouteConfig | Omit<RouteConfig, "body">> = {
14
14
  [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never;
15
15
  };
16
- declare const defineRouterConfig: <T extends RouterConfig>(config: T) => T;
16
+ declare const defineRoutes: <T extends RouterConfig>(routes: T) => T;
17
17
 
18
18
  type RouterClient<T extends RouterConfig> = {
19
19
  GET: <K extends keyof T["GET"]>(path: K, options?: Omit<Omit<InferRouteConfig<T["GET"][K]>, "body">, "result">) => Promise<InferRouteConfig<T["GET"][K]>["result"]>;
@@ -22,18 +22,19 @@ type RouterClient<T extends RouterConfig> = {
22
22
  type FetchFunction = (url: string, options: RequestInit) => Promise<Response>;
23
23
  type CreateClientOptions = {
24
24
  baseUrl: string;
25
- headers?: Record<string, string>;
25
+ getHeaders?: () => Promise<Record<string, string>> | Record<string, string>;
26
26
  fetch?: FetchFunction;
27
- validateRequest?: boolean;
27
+ validate?: boolean;
28
+ debug?: boolean;
28
29
  };
29
- declare const createClient: <T extends RouterConfig>(config: T, options: CreateClientOptions) => RouterClient<T>;
30
+ declare const createClient: <T extends RouterConfig>(routes: T, options: CreateClientOptions) => RouterClient<T>;
30
31
 
31
32
  type ExtractRouteParams<T extends string> = T extends `${infer _Start}:${infer Param}/${infer Rest}` ? {
32
33
  [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string;
33
34
  } : T extends `${infer _Start}:${infer Param}` ? {
34
35
  [K in Param]: string;
35
36
  } : Record<string, never>;
36
- type RouterHandlerConfig<T extends RouterConfig, TContext> = {
37
+ type RouteHandlers<T extends RouterConfig, TContext> = {
37
38
  GET: {
38
39
  [K in keyof T["GET"]]: (data: Omit<Omit<InferRouteConfig<T["GET"][K]>, "body">, "result"> & {
39
40
  params: K extends string ? ExtractRouteParams<K> : unknown;
@@ -45,8 +46,9 @@ type RouterHandlerConfig<T extends RouterConfig, TContext> = {
45
46
  }, ctx: TContext) => Promise<InferRouteConfig<T["POST"][K]>["result"]> | InferRouteConfig<T["POST"][K]>["result"];
46
47
  };
47
48
  };
48
- declare const applyConfigToExpressRouter: <T extends RouterConfig, TContext>(router: Router, config: T, handlers: RouterHandlerConfig<T, TContext> & {
49
+ declare const createExpressRouter: <T extends RouterConfig, TContext>(router: Router, routes: T, handlers: RouteHandlers<T, TContext> & {
49
50
  ctx?: (req: Request) => TContext;
51
+ schemaFilePath?: string;
50
52
  }) => Router;
51
53
 
52
- export { type InferRouteConfig, type RouteConfig, type RouterClient, type RouterConfig, type RouterHandlerConfig, applyConfigToExpressRouter, createClient, defineRouterConfig };
54
+ export { type InferRouteConfig, type RouteConfig, type RouteHandlers, type RouterClient, type RouterConfig, createClient, createExpressRouter, defineRoutes };
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ type RouterConfig = {
13
13
  type InferRouteConfig<T extends RouteConfig | Omit<RouteConfig, "body">> = {
14
14
  [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never;
15
15
  };
16
- declare const defineRouterConfig: <T extends RouterConfig>(config: T) => T;
16
+ declare const defineRoutes: <T extends RouterConfig>(routes: T) => T;
17
17
 
18
18
  type RouterClient<T extends RouterConfig> = {
19
19
  GET: <K extends keyof T["GET"]>(path: K, options?: Omit<Omit<InferRouteConfig<T["GET"][K]>, "body">, "result">) => Promise<InferRouteConfig<T["GET"][K]>["result"]>;
@@ -22,18 +22,19 @@ type RouterClient<T extends RouterConfig> = {
22
22
  type FetchFunction = (url: string, options: RequestInit) => Promise<Response>;
23
23
  type CreateClientOptions = {
24
24
  baseUrl: string;
25
- headers?: Record<string, string>;
25
+ getHeaders?: () => Promise<Record<string, string>> | Record<string, string>;
26
26
  fetch?: FetchFunction;
27
- validateRequest?: boolean;
27
+ validate?: boolean;
28
+ debug?: boolean;
28
29
  };
29
- declare const createClient: <T extends RouterConfig>(config: T, options: CreateClientOptions) => RouterClient<T>;
30
+ declare const createClient: <T extends RouterConfig>(routes: T, options: CreateClientOptions) => RouterClient<T>;
30
31
 
31
32
  type ExtractRouteParams<T extends string> = T extends `${infer _Start}:${infer Param}/${infer Rest}` ? {
32
33
  [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string;
33
34
  } : T extends `${infer _Start}:${infer Param}` ? {
34
35
  [K in Param]: string;
35
36
  } : Record<string, never>;
36
- type RouterHandlerConfig<T extends RouterConfig, TContext> = {
37
+ type RouteHandlers<T extends RouterConfig, TContext> = {
37
38
  GET: {
38
39
  [K in keyof T["GET"]]: (data: Omit<Omit<InferRouteConfig<T["GET"][K]>, "body">, "result"> & {
39
40
  params: K extends string ? ExtractRouteParams<K> : unknown;
@@ -45,8 +46,9 @@ type RouterHandlerConfig<T extends RouterConfig, TContext> = {
45
46
  }, ctx: TContext) => Promise<InferRouteConfig<T["POST"][K]>["result"]> | InferRouteConfig<T["POST"][K]>["result"];
46
47
  };
47
48
  };
48
- declare const applyConfigToExpressRouter: <T extends RouterConfig, TContext>(router: Router, config: T, handlers: RouterHandlerConfig<T, TContext> & {
49
+ declare const createExpressRouter: <T extends RouterConfig, TContext>(router: Router, routes: T, handlers: RouteHandlers<T, TContext> & {
49
50
  ctx?: (req: Request) => TContext;
51
+ schemaFilePath?: string;
50
52
  }) => Router;
51
53
 
52
- export { type InferRouteConfig, type RouteConfig, type RouterClient, type RouterConfig, type RouterHandlerConfig, applyConfigToExpressRouter, createClient, defineRouterConfig };
54
+ export { type InferRouteConfig, type RouteConfig, type RouteHandlers, type RouterClient, type RouterConfig, createClient, createExpressRouter, defineRoutes };
package/dist/index.js CHANGED
@@ -1,78 +1,82 @@
1
1
  'use strict';
2
2
 
3
+ var promises = require('fs/promises');
4
+
3
5
  // src/client.ts
4
- var createClient = (config, options) => {
6
+ var createClient = (routes, options) => {
5
7
  const {
6
8
  baseUrl,
7
- headers = {},
9
+ getHeaders = () => Promise.resolve({}),
8
10
  fetch: customFetch = fetch,
9
- validateRequest = false
11
+ validate = false
10
12
  } = options;
11
13
  const client = {
12
14
  GET: {},
13
15
  POST: {}
14
16
  };
15
- client.GET = async (path, data) => {
16
- if (validateRequest && data?.query) {
17
- config.GET[path]?.query?.parse(data.query);
17
+ client.GET = async (path, options2) => {
18
+ if (validate && options2?.query) {
19
+ routes.GET[path]?.query?.parse(options2.query);
18
20
  }
19
- const queryString = data?.query ? "?" + new URLSearchParams(data.query).toString() : "";
21
+ const queryString = options2?.query ? "?" + new URLSearchParams(options2.query).toString() : "";
20
22
  const response = await customFetch(`${baseUrl}${path}${queryString}`, {
21
23
  method: "GET",
22
24
  headers: {
23
25
  "Content-Type": "application/json",
24
- ...headers
26
+ ...await getHeaders()
25
27
  }
26
28
  });
27
29
  if (!response.ok) {
28
30
  const error = await response.json();
29
- console.error(error);
31
+ if (options2.debug) {
32
+ console.debug(error);
33
+ }
30
34
  throw new Error(error.message);
31
35
  }
32
36
  const json = await response.json();
33
- return config.GET[path]?.result.parse(json);
37
+ return routes.GET[path]?.result.parse(json);
34
38
  };
35
- client.POST = async (path, body, rest) => {
36
- if (validateRequest) {
39
+ client.POST = async (path, body, options2) => {
40
+ if (validate) {
37
41
  if (body) {
38
- config.POST[path]?.body?.parse(body);
42
+ routes.POST[path]?.body?.parse(body);
39
43
  }
40
- if (rest?.query) {
41
- config.POST[path]?.query?.parse(rest.query);
44
+ if (options2?.query) {
45
+ routes.POST[path]?.query?.parse(options2.query);
42
46
  }
43
47
  }
44
- const queryString = rest?.query ? "?" + new URLSearchParams(rest.query).toString() : "";
48
+ const queryString = options2?.query ? "?" + new URLSearchParams(options2.query).toString() : "";
45
49
  const response = await customFetch(`${baseUrl}${path}${queryString}`, {
46
50
  method: "POST",
47
51
  headers: {
48
52
  "Content-Type": "application/json",
49
- ...headers
53
+ ...await getHeaders()
50
54
  },
51
55
  body: JSON.stringify(body)
52
56
  });
53
57
  if (!response.ok) {
54
58
  const error = await response.json();
55
- console.error(error);
59
+ if (options2.debug) {
60
+ console.debug(error);
61
+ }
56
62
  throw new Error(error.message);
57
63
  }
58
64
  const json = await response.json();
59
- return config.POST[path]?.result.parse(json);
65
+ return routes.POST[path]?.result.parse(json);
60
66
  };
61
67
  return client;
62
68
  };
63
-
64
- // src/server.ts
65
- var applyConfigToExpressRouter = (router, config, handlers) => {
66
- router = Object.keys(config.GET).reduce(
69
+ var createExpressRouter = (router, routes, handlers) => {
70
+ router = Object.keys(routes.GET).reduce(
67
71
  (r, x) => r.get(x, async (req, res, next) => {
68
72
  try {
69
73
  const ctx = handlers.ctx?.(req) ?? {};
70
74
  const data = {
71
75
  params: req.params,
72
- query: config.GET[x]?.query?.parse(req.query)
76
+ query: routes.GET[x]?.query?.parse(req.query)
73
77
  };
74
78
  const result = await handlers.GET[x]?.(data, ctx);
75
- const validatedResult = config.GET[x]?.result.parse(result);
79
+ const validatedResult = routes.GET[x]?.result.parse(result);
76
80
  res.json(validatedResult);
77
81
  } catch (err) {
78
82
  next(err);
@@ -80,17 +84,23 @@ var applyConfigToExpressRouter = (router, config, handlers) => {
80
84
  }),
81
85
  router
82
86
  );
83
- router = Object.keys(config.POST).reduce(
87
+ if (handlers.schemaFilePath) {
88
+ router = router.get(
89
+ "/__schema",
90
+ async (_, res) => res.contentType("text/plain").send(await promises.readFile(handlers.schemaFilePath, "utf8"))
91
+ );
92
+ }
93
+ router = Object.keys(routes.POST).reduce(
84
94
  (r, x) => r.post(x, async (req, res, next) => {
85
95
  try {
86
96
  const ctx = handlers.ctx?.(req) ?? {};
87
97
  const data = {
88
98
  params: req.params,
89
- body: config.POST[x]?.body.parse(req.body),
90
- query: config.POST[x]?.query?.parse(req.query)
99
+ body: routes.POST[x]?.body.parse(req.body),
100
+ query: routes.POST[x]?.query?.parse(req.query)
91
101
  };
92
102
  const result = await handlers.POST[x]?.(data, ctx);
93
- const validatedResult = config.POST[x]?.result.parse(result);
103
+ const validatedResult = routes.POST[x]?.result.parse(result);
94
104
  res.json(validatedResult);
95
105
  } catch (err) {
96
106
  next(err);
@@ -102,10 +112,10 @@ var applyConfigToExpressRouter = (router, config, handlers) => {
102
112
  };
103
113
 
104
114
  // src/types.ts
105
- var defineRouterConfig = (config) => config;
115
+ var defineRoutes = (routes) => routes;
106
116
 
107
- exports.applyConfigToExpressRouter = applyConfigToExpressRouter;
108
117
  exports.createClient = createClient;
109
- exports.defineRouterConfig = defineRouterConfig;
118
+ exports.createExpressRouter = createExpressRouter;
119
+ exports.defineRoutes = defineRoutes;
110
120
  //# sourceMappingURL=index.js.map
111
121
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/server.ts","../src/types.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { InferRouteConfig, RouterConfig } from \"./types\"\n\nexport type RouterClient<T extends RouterConfig> = {\n GET: <K extends keyof T[\"GET\"]>(\n path: K,\n options?: Omit<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, \"result\">\n ) => Promise<InferRouteConfig<T[\"GET\"][K]>[\"result\"]>\n POST: <K extends keyof T[\"POST\"]>(\n path: K,\n body: InferRouteConfig<T[\"POST\"][K]>[\"body\"],\n options?: Omit<InferRouteConfig<T[\"POST\"][K]>, \"body\" | \"result\">\n ) => Promise<InferRouteConfig<T[\"POST\"][K]>[\"result\"]>\n}\n\ntype FetchFunction = (url: string, options: RequestInit) => Promise<Response>\n\ntype CreateClientOptions = {\n baseUrl: string\n headers?: Record<string, string>\n fetch?: FetchFunction\n validateRequest?: boolean\n}\n\nexport const createClient = <T extends RouterConfig>(\n config: T,\n options: CreateClientOptions\n): RouterClient<T> => {\n const {\n baseUrl,\n headers = {},\n fetch: customFetch = fetch,\n validateRequest = false,\n } = options\n\n const client = {\n GET: {} as any,\n POST: {} as any,\n }\n\n client.GET = async (path: string, data?: any) => {\n if (validateRequest && data?.query) {\n config.GET[path]?.query?.parse(data.query)\n }\n const queryString = data?.query\n ? \"?\" + new URLSearchParams(data.query).toString()\n : \"\"\n const response = await customFetch(`${baseUrl}${path}${queryString}`, {\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...headers,\n },\n })\n\n if (!response.ok) {\n const error: any = await response.json()\n console.error(error)\n throw new Error(error.message)\n }\n\n const json = await response.json()\n\n return config.GET[path]?.result.parse(json)\n }\n\n client.POST = async (path: string, body: any, rest?: any) => {\n if (validateRequest) {\n if (body) {\n config.POST[path]?.body?.parse(body)\n }\n if (rest?.query) {\n config.POST[path]?.query?.parse(rest.query)\n }\n }\n const queryString = rest?.query\n ? \"?\" + new URLSearchParams(rest.query).toString()\n : \"\"\n const response = await customFetch(`${baseUrl}${path}${queryString}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...headers,\n },\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n const error: any = await response.json()\n console.error(error)\n throw new Error(error.message)\n }\n\n const json = await response.json()\n\n return config.POST[path]?.result.parse(json)\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport type { InferRouteConfig, RouterConfig } from \"./types\"\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\ntype ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? { [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n\nexport type RouterHandlerConfig<T extends RouterConfig, TContext> = {\n GET: {\n [K in keyof T[\"GET\"]]: (\n data: Omit<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, \"result\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n },\n ctx: TContext\n ) =>\n | Promise<InferRouteConfig<T[\"GET\"][K]>[\"result\"]>\n | InferRouteConfig<T[\"GET\"][K]>[\"result\"]\n }\n POST: {\n [K in keyof T[\"POST\"]]: (\n data: Omit<InferRouteConfig<T[\"POST\"][K]>, \"result\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n },\n ctx: TContext\n ) =>\n | Promise<InferRouteConfig<T[\"POST\"][K]>[\"result\"]>\n | InferRouteConfig<T[\"POST\"][K]>[\"result\"]\n }\n}\n\nexport const applyConfigToExpressRouter = <T extends RouterConfig, TContext>(\n router: Router,\n config: T,\n handlers: RouterHandlerConfig<T, TContext> & {\n ctx?: (req: Request) => TContext\n }\n) => {\n router = Object.keys(config.GET).reduce(\n (r, x) =>\n r.get(x, async (req, res, next) => {\n try {\n const ctx = (handlers.ctx?.(req) ?? {}) as TContext\n\n const data = {\n params: req.params,\n query: config.GET[x]?.query?.parse(req.query),\n }\n const result = await handlers.GET[x]?.(data as any, ctx)\n const validatedResult = config.GET[x]?.result.parse(result)\n res.json(validatedResult)\n } catch (err) {\n next(err)\n }\n }),\n router\n )\n\n router = Object.keys(config.POST).reduce(\n (r, x) =>\n r.post(x, async (req, res, next) => {\n try {\n const ctx = (handlers.ctx?.(req) ?? {}) as TContext\n\n const data = {\n params: req.params,\n body: config.POST[x]?.body.parse(req.body),\n query: config.POST[x]?.query?.parse(req.query),\n }\n const result = await handlers.POST[x]?.(data as any, ctx)\n const validatedResult = config.POST[x]?.result.parse(result)\n res.json(validatedResult)\n } catch (err) {\n next(err)\n }\n }),\n router\n )\n\n return router\n}\n","import type z from \"zod\";\n\nexport type RouteConfig = {\n body: z.ZodType;\n query?: z.ZodType;\n result: z.ZodType;\n};\n\nexport type RouterConfig = {\n GET: Record<string, Omit<RouteConfig, \"body\">>;\n POST: Record<string, RouteConfig>;\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 defineRouterConfig = <T extends RouterConfig>(config: T): T =>\n config;\n"]}
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 type { InferRouteConfig, RouterConfig } from \"./types\"\n\nexport type RouterClient<T extends RouterConfig> = {\n GET: <K extends keyof T[\"GET\"]>(\n path: K,\n options?: Omit<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, \"result\">\n ) => Promise<InferRouteConfig<T[\"GET\"][K]>[\"result\"]>\n POST: <K extends keyof T[\"POST\"]>(\n path: K,\n body: InferRouteConfig<T[\"POST\"][K]>[\"body\"],\n options?: Omit<InferRouteConfig<T[\"POST\"][K]>, \"body\" | \"result\">\n ) => Promise<InferRouteConfig<T[\"POST\"][K]>[\"result\"]>\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\nexport const createClient = <T extends 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 client = {\n GET: {} as any,\n POST: {} as any,\n }\n\n client.GET = async (path: string, options?: any) => {\n if (validate && options?.query) {\n routes.GET[path]?.query?.parse(options.query)\n }\n\n const queryString = options?.query\n ? \"?\" + new URLSearchParams(options.query).toString()\n : \"\"\n\n const response = await customFetch(`${baseUrl}${path}${queryString}`, {\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n })\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 json = await response.json()\n\n return routes.GET[path]?.result.parse(json)\n }\n\n client.POST = async (path: string, body: any, options?: any) => {\n if (validate) {\n if (body) {\n routes.POST[path]?.body?.parse(body)\n }\n if (options?.query) {\n routes.POST[path]?.query?.parse(options.query)\n }\n }\n\n const queryString = options?.query\n ? \"?\" + new URLSearchParams(options.query).toString()\n : \"\"\n\n const response = await customFetch(`${baseUrl}${path}${queryString}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n body: JSON.stringify(body),\n })\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 json = await response.json()\n\n return routes.POST[path]?.result.parse(json)\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport { readFile } from \"node:fs/promises\"\nimport type { InferRouteConfig, RouterConfig } from \"./types\"\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\ntype ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? { [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n\nexport type RouteHandlers<T extends RouterConfig, TContext> = {\n GET: {\n [K in keyof T[\"GET\"]]: (\n data: Omit<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, \"result\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n },\n ctx: TContext\n ) =>\n | Promise<InferRouteConfig<T[\"GET\"][K]>[\"result\"]>\n | InferRouteConfig<T[\"GET\"][K]>[\"result\"]\n }\n POST: {\n [K in keyof T[\"POST\"]]: (\n data: Omit<InferRouteConfig<T[\"POST\"][K]>, \"result\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n },\n ctx: TContext\n ) =>\n | Promise<InferRouteConfig<T[\"POST\"][K]>[\"result\"]>\n | InferRouteConfig<T[\"POST\"][K]>[\"result\"]\n }\n}\n\nexport const createExpressRouter = <T extends RouterConfig, TContext>(\n router: Router,\n routes: T,\n handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n schemaFilePath?: string\n }\n) => {\n router = Object.keys(routes.GET).reduce(\n (r, x) =>\n r.get(x, async (req, res, next) => {\n try {\n const ctx = (handlers.ctx?.(req) ?? {}) as TContext\n\n const data = {\n params: req.params,\n query: routes.GET[x]?.query?.parse(req.query),\n }\n const result = await handlers.GET[x]?.(data as any, ctx)\n const validatedResult = routes.GET[x]?.result.parse(result)\n\n res.json(validatedResult)\n } catch (err) {\n next(err)\n }\n }),\n router\n )\n\n if (handlers.schemaFilePath) {\n router = router.get(\"/__schema\", async (_, res) =>\n res\n .contentType(\"text/plain\")\n .send(await readFile(handlers.schemaFilePath!, \"utf8\"))\n )\n }\n\n router = Object.keys(routes.POST).reduce(\n (r, x) =>\n r.post(x, async (req, res, next) => {\n try {\n const ctx = (handlers.ctx?.(req) ?? {}) as TContext\n\n const data = {\n params: req.params,\n body: routes.POST[x]?.body.parse(req.body),\n query: routes.POST[x]?.query?.parse(req.query),\n }\n const result = await handlers.POST[x]?.(data as any, ctx)\n const validatedResult = routes.POST[x]?.result.parse(result)\n res.json(validatedResult)\n } catch (err) {\n next(err)\n }\n }),\n router\n )\n\n return router\n}\n","import type z from \"zod\"\n\nexport type RouteConfig = {\n body: z.ZodType\n query?: z.ZodType\n result: z.ZodType\n}\n\nexport type RouterConfig = {\n GET: Record<string, Omit<RouteConfig, \"body\">>\n POST: Record<string, RouteConfig>\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 RouterConfig>(routes: T): T =>\n routes\n"]}
package/dist/index.mjs CHANGED
@@ -1,76 +1,80 @@
1
+ import { readFile } from 'fs/promises';
2
+
1
3
  // src/client.ts
2
- var createClient = (config, options) => {
4
+ var createClient = (routes, options) => {
3
5
  const {
4
6
  baseUrl,
5
- headers = {},
7
+ getHeaders = () => Promise.resolve({}),
6
8
  fetch: customFetch = fetch,
7
- validateRequest = false
9
+ validate = false
8
10
  } = options;
9
11
  const client = {
10
12
  GET: {},
11
13
  POST: {}
12
14
  };
13
- client.GET = async (path, data) => {
14
- if (validateRequest && data?.query) {
15
- config.GET[path]?.query?.parse(data.query);
15
+ client.GET = async (path, options2) => {
16
+ if (validate && options2?.query) {
17
+ routes.GET[path]?.query?.parse(options2.query);
16
18
  }
17
- const queryString = data?.query ? "?" + new URLSearchParams(data.query).toString() : "";
19
+ const queryString = options2?.query ? "?" + new URLSearchParams(options2.query).toString() : "";
18
20
  const response = await customFetch(`${baseUrl}${path}${queryString}`, {
19
21
  method: "GET",
20
22
  headers: {
21
23
  "Content-Type": "application/json",
22
- ...headers
24
+ ...await getHeaders()
23
25
  }
24
26
  });
25
27
  if (!response.ok) {
26
28
  const error = await response.json();
27
- console.error(error);
29
+ if (options2.debug) {
30
+ console.debug(error);
31
+ }
28
32
  throw new Error(error.message);
29
33
  }
30
34
  const json = await response.json();
31
- return config.GET[path]?.result.parse(json);
35
+ return routes.GET[path]?.result.parse(json);
32
36
  };
33
- client.POST = async (path, body, rest) => {
34
- if (validateRequest) {
37
+ client.POST = async (path, body, options2) => {
38
+ if (validate) {
35
39
  if (body) {
36
- config.POST[path]?.body?.parse(body);
40
+ routes.POST[path]?.body?.parse(body);
37
41
  }
38
- if (rest?.query) {
39
- config.POST[path]?.query?.parse(rest.query);
42
+ if (options2?.query) {
43
+ routes.POST[path]?.query?.parse(options2.query);
40
44
  }
41
45
  }
42
- const queryString = rest?.query ? "?" + new URLSearchParams(rest.query).toString() : "";
46
+ const queryString = options2?.query ? "?" + new URLSearchParams(options2.query).toString() : "";
43
47
  const response = await customFetch(`${baseUrl}${path}${queryString}`, {
44
48
  method: "POST",
45
49
  headers: {
46
50
  "Content-Type": "application/json",
47
- ...headers
51
+ ...await getHeaders()
48
52
  },
49
53
  body: JSON.stringify(body)
50
54
  });
51
55
  if (!response.ok) {
52
56
  const error = await response.json();
53
- console.error(error);
57
+ if (options2.debug) {
58
+ console.debug(error);
59
+ }
54
60
  throw new Error(error.message);
55
61
  }
56
62
  const json = await response.json();
57
- return config.POST[path]?.result.parse(json);
63
+ return routes.POST[path]?.result.parse(json);
58
64
  };
59
65
  return client;
60
66
  };
61
-
62
- // src/server.ts
63
- var applyConfigToExpressRouter = (router, config, handlers) => {
64
- router = Object.keys(config.GET).reduce(
67
+ var createExpressRouter = (router, routes, handlers) => {
68
+ router = Object.keys(routes.GET).reduce(
65
69
  (r, x) => r.get(x, async (req, res, next) => {
66
70
  try {
67
71
  const ctx = handlers.ctx?.(req) ?? {};
68
72
  const data = {
69
73
  params: req.params,
70
- query: config.GET[x]?.query?.parse(req.query)
74
+ query: routes.GET[x]?.query?.parse(req.query)
71
75
  };
72
76
  const result = await handlers.GET[x]?.(data, ctx);
73
- const validatedResult = config.GET[x]?.result.parse(result);
77
+ const validatedResult = routes.GET[x]?.result.parse(result);
74
78
  res.json(validatedResult);
75
79
  } catch (err) {
76
80
  next(err);
@@ -78,17 +82,23 @@ var applyConfigToExpressRouter = (router, config, handlers) => {
78
82
  }),
79
83
  router
80
84
  );
81
- router = Object.keys(config.POST).reduce(
85
+ if (handlers.schemaFilePath) {
86
+ router = router.get(
87
+ "/__schema",
88
+ async (_, res) => res.contentType("text/plain").send(await readFile(handlers.schemaFilePath, "utf8"))
89
+ );
90
+ }
91
+ router = Object.keys(routes.POST).reduce(
82
92
  (r, x) => r.post(x, async (req, res, next) => {
83
93
  try {
84
94
  const ctx = handlers.ctx?.(req) ?? {};
85
95
  const data = {
86
96
  params: req.params,
87
- body: config.POST[x]?.body.parse(req.body),
88
- query: config.POST[x]?.query?.parse(req.query)
97
+ body: routes.POST[x]?.body.parse(req.body),
98
+ query: routes.POST[x]?.query?.parse(req.query)
89
99
  };
90
100
  const result = await handlers.POST[x]?.(data, ctx);
91
- const validatedResult = config.POST[x]?.result.parse(result);
101
+ const validatedResult = routes.POST[x]?.result.parse(result);
92
102
  res.json(validatedResult);
93
103
  } catch (err) {
94
104
  next(err);
@@ -100,8 +110,8 @@ var applyConfigToExpressRouter = (router, config, handlers) => {
100
110
  };
101
111
 
102
112
  // src/types.ts
103
- var defineRouterConfig = (config) => config;
113
+ var defineRoutes = (routes) => routes;
104
114
 
105
- export { applyConfigToExpressRouter, createClient, defineRouterConfig };
115
+ export { createClient, createExpressRouter, defineRoutes };
106
116
  //# sourceMappingURL=index.mjs.map
107
117
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/server.ts","../src/types.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import type { InferRouteConfig, RouterConfig } from \"./types\"\n\nexport type RouterClient<T extends RouterConfig> = {\n GET: <K extends keyof T[\"GET\"]>(\n path: K,\n options?: Omit<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, \"result\">\n ) => Promise<InferRouteConfig<T[\"GET\"][K]>[\"result\"]>\n POST: <K extends keyof T[\"POST\"]>(\n path: K,\n body: InferRouteConfig<T[\"POST\"][K]>[\"body\"],\n options?: Omit<InferRouteConfig<T[\"POST\"][K]>, \"body\" | \"result\">\n ) => Promise<InferRouteConfig<T[\"POST\"][K]>[\"result\"]>\n}\n\ntype FetchFunction = (url: string, options: RequestInit) => Promise<Response>\n\ntype CreateClientOptions = {\n baseUrl: string\n headers?: Record<string, string>\n fetch?: FetchFunction\n validateRequest?: boolean\n}\n\nexport const createClient = <T extends RouterConfig>(\n config: T,\n options: CreateClientOptions\n): RouterClient<T> => {\n const {\n baseUrl,\n headers = {},\n fetch: customFetch = fetch,\n validateRequest = false,\n } = options\n\n const client = {\n GET: {} as any,\n POST: {} as any,\n }\n\n client.GET = async (path: string, data?: any) => {\n if (validateRequest && data?.query) {\n config.GET[path]?.query?.parse(data.query)\n }\n const queryString = data?.query\n ? \"?\" + new URLSearchParams(data.query).toString()\n : \"\"\n const response = await customFetch(`${baseUrl}${path}${queryString}`, {\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...headers,\n },\n })\n\n if (!response.ok) {\n const error: any = await response.json()\n console.error(error)\n throw new Error(error.message)\n }\n\n const json = await response.json()\n\n return config.GET[path]?.result.parse(json)\n }\n\n client.POST = async (path: string, body: any, rest?: any) => {\n if (validateRequest) {\n if (body) {\n config.POST[path]?.body?.parse(body)\n }\n if (rest?.query) {\n config.POST[path]?.query?.parse(rest.query)\n }\n }\n const queryString = rest?.query\n ? \"?\" + new URLSearchParams(rest.query).toString()\n : \"\"\n const response = await customFetch(`${baseUrl}${path}${queryString}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...headers,\n },\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n const error: any = await response.json()\n console.error(error)\n throw new Error(error.message)\n }\n\n const json = await response.json()\n\n return config.POST[path]?.result.parse(json)\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport type { InferRouteConfig, RouterConfig } from \"./types\"\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\ntype ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? { [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n\nexport type RouterHandlerConfig<T extends RouterConfig, TContext> = {\n GET: {\n [K in keyof T[\"GET\"]]: (\n data: Omit<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, \"result\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n },\n ctx: TContext\n ) =>\n | Promise<InferRouteConfig<T[\"GET\"][K]>[\"result\"]>\n | InferRouteConfig<T[\"GET\"][K]>[\"result\"]\n }\n POST: {\n [K in keyof T[\"POST\"]]: (\n data: Omit<InferRouteConfig<T[\"POST\"][K]>, \"result\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n },\n ctx: TContext\n ) =>\n | Promise<InferRouteConfig<T[\"POST\"][K]>[\"result\"]>\n | InferRouteConfig<T[\"POST\"][K]>[\"result\"]\n }\n}\n\nexport const applyConfigToExpressRouter = <T extends RouterConfig, TContext>(\n router: Router,\n config: T,\n handlers: RouterHandlerConfig<T, TContext> & {\n ctx?: (req: Request) => TContext\n }\n) => {\n router = Object.keys(config.GET).reduce(\n (r, x) =>\n r.get(x, async (req, res, next) => {\n try {\n const ctx = (handlers.ctx?.(req) ?? {}) as TContext\n\n const data = {\n params: req.params,\n query: config.GET[x]?.query?.parse(req.query),\n }\n const result = await handlers.GET[x]?.(data as any, ctx)\n const validatedResult = config.GET[x]?.result.parse(result)\n res.json(validatedResult)\n } catch (err) {\n next(err)\n }\n }),\n router\n )\n\n router = Object.keys(config.POST).reduce(\n (r, x) =>\n r.post(x, async (req, res, next) => {\n try {\n const ctx = (handlers.ctx?.(req) ?? {}) as TContext\n\n const data = {\n params: req.params,\n body: config.POST[x]?.body.parse(req.body),\n query: config.POST[x]?.query?.parse(req.query),\n }\n const result = await handlers.POST[x]?.(data as any, ctx)\n const validatedResult = config.POST[x]?.result.parse(result)\n res.json(validatedResult)\n } catch (err) {\n next(err)\n }\n }),\n router\n )\n\n return router\n}\n","import type z from \"zod\";\n\nexport type RouteConfig = {\n body: z.ZodType;\n query?: z.ZodType;\n result: z.ZodType;\n};\n\nexport type RouterConfig = {\n GET: Record<string, Omit<RouteConfig, \"body\">>;\n POST: Record<string, RouteConfig>;\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 defineRouterConfig = <T extends RouterConfig>(config: T): T =>\n config;\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 type { InferRouteConfig, RouterConfig } from \"./types\"\n\nexport type RouterClient<T extends RouterConfig> = {\n GET: <K extends keyof T[\"GET\"]>(\n path: K,\n options?: Omit<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, \"result\">\n ) => Promise<InferRouteConfig<T[\"GET\"][K]>[\"result\"]>\n POST: <K extends keyof T[\"POST\"]>(\n path: K,\n body: InferRouteConfig<T[\"POST\"][K]>[\"body\"],\n options?: Omit<InferRouteConfig<T[\"POST\"][K]>, \"body\" | \"result\">\n ) => Promise<InferRouteConfig<T[\"POST\"][K]>[\"result\"]>\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\nexport const createClient = <T extends 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 client = {\n GET: {} as any,\n POST: {} as any,\n }\n\n client.GET = async (path: string, options?: any) => {\n if (validate && options?.query) {\n routes.GET[path]?.query?.parse(options.query)\n }\n\n const queryString = options?.query\n ? \"?\" + new URLSearchParams(options.query).toString()\n : \"\"\n\n const response = await customFetch(`${baseUrl}${path}${queryString}`, {\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n })\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 json = await response.json()\n\n return routes.GET[path]?.result.parse(json)\n }\n\n client.POST = async (path: string, body: any, options?: any) => {\n if (validate) {\n if (body) {\n routes.POST[path]?.body?.parse(body)\n }\n if (options?.query) {\n routes.POST[path]?.query?.parse(options.query)\n }\n }\n\n const queryString = options?.query\n ? \"?\" + new URLSearchParams(options.query).toString()\n : \"\"\n\n const response = await customFetch(`${baseUrl}${path}${queryString}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n body: JSON.stringify(body),\n })\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 json = await response.json()\n\n return routes.POST[path]?.result.parse(json)\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport { readFile } from \"node:fs/promises\"\nimport type { InferRouteConfig, RouterConfig } from \"./types\"\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\ntype ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? { [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n\nexport type RouteHandlers<T extends RouterConfig, TContext> = {\n GET: {\n [K in keyof T[\"GET\"]]: (\n data: Omit<Omit<InferRouteConfig<T[\"GET\"][K]>, \"body\">, \"result\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n },\n ctx: TContext\n ) =>\n | Promise<InferRouteConfig<T[\"GET\"][K]>[\"result\"]>\n | InferRouteConfig<T[\"GET\"][K]>[\"result\"]\n }\n POST: {\n [K in keyof T[\"POST\"]]: (\n data: Omit<InferRouteConfig<T[\"POST\"][K]>, \"result\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n },\n ctx: TContext\n ) =>\n | Promise<InferRouteConfig<T[\"POST\"][K]>[\"result\"]>\n | InferRouteConfig<T[\"POST\"][K]>[\"result\"]\n }\n}\n\nexport const createExpressRouter = <T extends RouterConfig, TContext>(\n router: Router,\n routes: T,\n handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n schemaFilePath?: string\n }\n) => {\n router = Object.keys(routes.GET).reduce(\n (r, x) =>\n r.get(x, async (req, res, next) => {\n try {\n const ctx = (handlers.ctx?.(req) ?? {}) as TContext\n\n const data = {\n params: req.params,\n query: routes.GET[x]?.query?.parse(req.query),\n }\n const result = await handlers.GET[x]?.(data as any, ctx)\n const validatedResult = routes.GET[x]?.result.parse(result)\n\n res.json(validatedResult)\n } catch (err) {\n next(err)\n }\n }),\n router\n )\n\n if (handlers.schemaFilePath) {\n router = router.get(\"/__schema\", async (_, res) =>\n res\n .contentType(\"text/plain\")\n .send(await readFile(handlers.schemaFilePath!, \"utf8\"))\n )\n }\n\n router = Object.keys(routes.POST).reduce(\n (r, x) =>\n r.post(x, async (req, res, next) => {\n try {\n const ctx = (handlers.ctx?.(req) ?? {}) as TContext\n\n const data = {\n params: req.params,\n body: routes.POST[x]?.body.parse(req.body),\n query: routes.POST[x]?.query?.parse(req.query),\n }\n const result = await handlers.POST[x]?.(data as any, ctx)\n const validatedResult = routes.POST[x]?.result.parse(result)\n res.json(validatedResult)\n } catch (err) {\n next(err)\n }\n }),\n router\n )\n\n return router\n}\n","import type z from \"zod\"\n\nexport type RouteConfig = {\n body: z.ZodType\n query?: z.ZodType\n result: z.ZodType\n}\n\nexport type RouterConfig = {\n GET: Record<string, Omit<RouteConfig, \"body\">>\n POST: Record<string, RouteConfig>\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 RouterConfig>(routes: T): T =>\n routes\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jokio/rpc",
3
- "version": "0.3.1",
3
+ "version": "0.4.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",