rouzer 0.0.0 → 1.0.0-beta.10

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.
@@ -0,0 +1,36 @@
1
+ import type { Promisable, RouteRequest } from '../types.js';
2
+ export declare function createClient(config: {
3
+ /**
4
+ * Base URL to use for all requests.
5
+ */
6
+ baseURL: string;
7
+ /**
8
+ * Default headers to send with every request.
9
+ */
10
+ headers?: Record<string, string>;
11
+ /**
12
+ * Custom handler for non-200 response to a `.json()` request. By default, the
13
+ * response is always parsed as JSON, regardless of the HTTP status code.
14
+ */
15
+ onJsonError?: (response: Response) => Promisable<Response>;
16
+ }): {
17
+ config: {
18
+ /**
19
+ * Base URL to use for all requests.
20
+ */
21
+ baseURL: string;
22
+ /**
23
+ * Default headers to send with every request.
24
+ */
25
+ headers?: Record<string, string> | undefined;
26
+ /**
27
+ * Custom handler for non-200 response to a `.json()` request. By default, the
28
+ * response is always parsed as JSON, regardless of the HTTP status code.
29
+ */
30
+ onJsonError?: ((response: Response) => Promisable<Response>) | undefined;
31
+ };
32
+ request<T extends RouteRequest>({ path: pathBuilder, method, args: { path, query, body, headers }, route, }: T): Promise<Response & {
33
+ json(): Promise<T["$result"]>;
34
+ }>;
35
+ json<T extends RouteRequest>(request: T): Promise<T["$result"]>;
36
+ };
@@ -0,0 +1,55 @@
1
+ import { shake } from '../common.js';
2
+ export function createClient(config) {
3
+ const baseURL = config.baseURL.replace(/\/$/, '');
4
+ return {
5
+ config,
6
+ request({ path: pathBuilder, method, args: { path, query, body, headers }, route, }) {
7
+ if (route.path) {
8
+ path = route.path.parse(path);
9
+ }
10
+ let url;
11
+ const href = pathBuilder.href(path);
12
+ if (href[0] === '/') {
13
+ url = new URL(baseURL);
14
+ url.pathname += pathBuilder.href(path);
15
+ }
16
+ else {
17
+ url = new URL(href);
18
+ }
19
+ if (route.query) {
20
+ query = route.query.parse(query ?? {});
21
+ url.search = new URLSearchParams(query).toString();
22
+ }
23
+ else if (query) {
24
+ throw new Error('Unexpected query parameters');
25
+ }
26
+ if (route.body) {
27
+ body = route.body.parse(body !== undefined ? body : {});
28
+ }
29
+ else if (body !== undefined) {
30
+ throw new Error('Unexpected body');
31
+ }
32
+ if (config.headers || headers) {
33
+ headers = {
34
+ ...config.headers,
35
+ ...(headers && shake(headers)),
36
+ };
37
+ }
38
+ if (route.headers) {
39
+ headers = route.headers.parse(headers);
40
+ }
41
+ return fetch(url, {
42
+ method,
43
+ body: body !== undefined ? JSON.stringify(body) : undefined,
44
+ headers: headers,
45
+ });
46
+ },
47
+ async json(request) {
48
+ const response = await this.request(request);
49
+ if (!response.ok && config.onJsonError) {
50
+ return config.onJsonError(response);
51
+ }
52
+ return response.json();
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Map over all the keys to create a new object.
3
+ *
4
+ * @see https://radashi.js.org/reference/object/mapEntries
5
+ * @example
6
+ * ```ts
7
+ * const a = { a: 1, b: 2, c: 3 }
8
+ * mapEntries(a, (key, value) => [value, key])
9
+ * // => { 1: 'a', 2: 'b', 3: 'c' }
10
+ * ```
11
+ * @version 12.1.0
12
+ */
13
+ export declare function mapEntries<TKey extends string | number | symbol, TValue, TNewKey extends string | number | symbol, TNewValue>(obj: Record<TKey, TValue>, toEntry: (key: TKey, value: TValue) => [TNewKey, TNewValue]): Record<TNewKey, TNewValue>;
14
+ /**
15
+ * Removes (shakes out) undefined entries from an object. Optional
16
+ * second argument shakes out values by custom evaluation.
17
+ *
18
+ * Note that non-enumerable keys are never shaken out.
19
+ *
20
+ * @see https://radashi.js.org/reference/object/shake
21
+ * @example
22
+ * ```ts
23
+ * const a = { a: 1, b: undefined, c: 3 }
24
+ * shake(a)
25
+ * // => { a: 1, c: 3 }
26
+ * ```
27
+ * @version 12.1.0
28
+ */
29
+ export declare function shake<T extends object>(obj: T): {
30
+ [K in keyof T]: Exclude<T[K], undefined>;
31
+ };
32
+ export declare function shake<T extends object>(obj: T, filter: ((value: unknown) => boolean) | undefined): T;
33
+ /**
34
+ * Map over all the keys to create a new object.
35
+ *
36
+ * @see https://radashi.js.org/reference/object/mapValues
37
+ * @example
38
+ * ```ts
39
+ * const a = { a: 1, b: 2, c: 3 }
40
+ * mapValues(a, (value, key) => value * 2)
41
+ * // => { a: 2, b: 4, c: 6 }
42
+ * ```
43
+ * @version 12.1.0
44
+ */
45
+ export declare function mapValues<T extends object, U>(obj: T, mapFunc: (value: Required<T>[keyof T], key: keyof T) => U): {
46
+ [K in keyof T]: U;
47
+ };
package/dist/common.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Map over all the keys to create a new object.
3
+ *
4
+ * @see https://radashi.js.org/reference/object/mapEntries
5
+ * @example
6
+ * ```ts
7
+ * const a = { a: 1, b: 2, c: 3 }
8
+ * mapEntries(a, (key, value) => [value, key])
9
+ * // => { 1: 'a', 2: 'b', 3: 'c' }
10
+ * ```
11
+ * @version 12.1.0
12
+ */
13
+ export function mapEntries(obj, toEntry) {
14
+ if (!obj) {
15
+ return {};
16
+ }
17
+ return Object.entries(obj).reduce((acc, [key, value]) => {
18
+ const [newKey, newValue] = toEntry(key, value);
19
+ acc[newKey] = newValue;
20
+ return acc;
21
+ }, {});
22
+ }
23
+ export function shake(obj, filter = value => value === undefined) {
24
+ if (!obj) {
25
+ return {};
26
+ }
27
+ return Object.keys(obj).reduce((acc, key) => {
28
+ if (!filter(obj[key])) {
29
+ acc[key] = obj[key];
30
+ }
31
+ return acc;
32
+ }, {});
33
+ }
34
+ /**
35
+ * Map over all the keys to create a new object.
36
+ *
37
+ * @see https://radashi.js.org/reference/object/mapValues
38
+ * @example
39
+ * ```ts
40
+ * const a = { a: 1, b: 2, c: 3 }
41
+ * mapValues(a, (value, key) => value * 2)
42
+ * // => { a: 2, b: 4, c: 6 }
43
+ * ```
44
+ * @version 12.1.0
45
+ */
46
+ export function mapValues(obj, mapFunc) {
47
+ return Object.keys(obj).reduce((acc, key) => {
48
+ acc[key] = mapFunc(obj[key], key);
49
+ return acc;
50
+ }, {});
51
+ }
@@ -0,0 +1,5 @@
1
+ export * from './route.js';
2
+ export * from './server/router.js';
3
+ export * from './client/index.js';
4
+ export type * from './types.js';
5
+ export * from 'alien-middleware';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './route.js';
2
+ export * from './server/router.js';
3
+ export * from './client/index.js';
4
+ export * from 'alien-middleware';
@@ -0,0 +1,7 @@
1
+ import { RoutePattern } from '@remix-run/route-pattern';
2
+ import type { MutationMethod, QueryMethod, RouteFunction, RouteMethods, Unchecked } from './types.js';
3
+ export declare function $type<T>(): Unchecked<T>;
4
+ export declare function route<P extends string, T extends RouteMethods>(pattern: P, methods: T): {
5
+ path: RoutePattern<P>;
6
+ methods: T;
7
+ } & { [K in keyof T]: RouteFunction<Extract<T[K], MutationMethod | QueryMethod>>; };
package/dist/route.js ADDED
@@ -0,0 +1,18 @@
1
+ import { RoutePattern } from '@remix-run/route-pattern';
2
+ import { mapEntries } from './common.js';
3
+ export function $type() {
4
+ return null;
5
+ }
6
+ export function route(pattern, methods) {
7
+ const path = new RoutePattern(pattern);
8
+ const createFetch = (method, route) => (args) => {
9
+ return {
10
+ route,
11
+ path,
12
+ method,
13
+ args,
14
+ $result: undefined,
15
+ };
16
+ };
17
+ return Object.assign({ path, methods }, mapEntries(methods, (method, route) => [method, createFetch(method, route)]));
18
+ }
@@ -0,0 +1,111 @@
1
+ import type { AdapterRequestContext } from '@hattip/core';
2
+ import { type Params } from '@remix-run/route-pattern';
3
+ import { chain, MiddlewareChain, type MiddlewareContext } from 'alien-middleware';
4
+ import * as z from 'zod/mini';
5
+ import type { InferRouteResponse, MutationMethod, Promisable, QueryMethod, Routes } from '../types.js';
6
+ export { chain };
7
+ type EmptyMiddlewareChain<TPlatform = unknown> = MiddlewareChain<{
8
+ initial: {
9
+ env: {};
10
+ properties: {};
11
+ };
12
+ current: {
13
+ env: {};
14
+ properties: {};
15
+ };
16
+ platform: TPlatform;
17
+ }>;
18
+ export type RouterConfig = {
19
+ /**
20
+ * Base path to prepend to all routes.
21
+ * @example
22
+ * ```ts
23
+ * basePath: 'api/',
24
+ * ```
25
+ */
26
+ basePath?: string;
27
+ /**
28
+ * Routes to match.
29
+ * @example
30
+ * ```ts
31
+ * // This namespace contains your `route()` declarations.
32
+ * // Pass it to the `createRouter` function.
33
+ * import * as routes from './routes'
34
+ *
35
+ * createRouter({ routes })({
36
+ * // your route handlers...
37
+ * })
38
+ * ```
39
+ */
40
+ routes: Routes;
41
+ /**
42
+ * Middleware to apply to all routes.
43
+ * @see https://github.com/alien-rpc/alien-middleware#quick-start
44
+ * @example
45
+ * ```ts
46
+ * middlewares: chain().use(ctx => {
47
+ * return {
48
+ * db: postgres(ctx.env('POSTGRES_URL')),
49
+ * }
50
+ * }),
51
+ * ```
52
+ */
53
+ middlewares?: MiddlewareChain;
54
+ /**
55
+ * Enable debugging features.
56
+ * - When a handler throws an error, include its message in the response body.
57
+ * - Throw an error if a handler is not found for a route.
58
+ * @example
59
+ * ```ts
60
+ * debug: process.env.NODE_ENV !== 'production',
61
+ * ```
62
+ */
63
+ debug?: boolean;
64
+ /**
65
+ * CORS configuration.
66
+ */
67
+ cors?: {
68
+ /**
69
+ * If defined, requests must have an `Origin` header that is in this list.
70
+ *
71
+ * Origins may contain wildcards for protocol and subdomain. The protocol is
72
+ * optional and defaults to `https`.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * allowOrigins: ['example.net', 'https://*.example.com', '*://localhost:3000']
77
+ * ```
78
+ */
79
+ allowOrigins?: string[];
80
+ };
81
+ };
82
+ interface CreateRouterConfig<TRoutes extends Routes, TMiddleware extends MiddlewareChain> extends RouterConfig {
83
+ routes: TRoutes;
84
+ middlewares?: TMiddleware;
85
+ }
86
+ export declare function createRouter<TRoutes extends Routes, TMiddleware extends MiddlewareChain = EmptyMiddlewareChain>(config: CreateRouterConfig<TRoutes, TMiddleware>): (handlers: { [K in keyof TRoutes]: { [M in keyof TRoutes[K]["methods"]]: TRoutes[K]["methods"][M] extends infer T ? T extends TRoutes[K]["methods"][M] ? T extends QueryMethod ? (context: MiddlewareContext<TMiddleware> & {
87
+ path: T extends {
88
+ path: any;
89
+ } ? z.infer<T["path"]> : Params<TRoutes[K]["path"]["source"]>;
90
+ query: z.infer<T["query"]>;
91
+ headers: z.infer<T["headers"]>;
92
+ }) => Promisable<Response | InferRouteResponse<T>> : T extends MutationMethod ? (context: MiddlewareContext<TMiddleware> & {
93
+ path: T extends {
94
+ path: any;
95
+ } ? z.infer<T["path"]> : Params<TRoutes[K]["path"]["source"]>;
96
+ body: z.infer<T["body"]>;
97
+ headers: z.infer<T["headers"]>;
98
+ }) => Promisable<Response | InferRouteResponse<T>> : never : never : never; }; }) => import("alien-middleware").ApplyMiddleware<TMiddleware, (context: AdapterRequestContext<TMiddleware extends MiddlewareChain<infer T extends {
99
+ initial: {
100
+ env: object;
101
+ properties: object;
102
+ };
103
+ current: {
104
+ env: object;
105
+ properties: object;
106
+ };
107
+ platform: unknown;
108
+ }> ? T["platform"] : never> & {
109
+ url?: URL | undefined;
110
+ path?: {} | undefined;
111
+ }) => Promise<Response | undefined>>;
@@ -0,0 +1,175 @@
1
+ import { RoutePattern } from '@remix-run/route-pattern';
2
+ import { chain, } from 'alien-middleware';
3
+ import { mapValues } from '../common.js';
4
+ import * as z from 'zod/mini';
5
+ export { chain };
6
+ export function createRouter(config) {
7
+ const keys = Object.keys(config.routes);
8
+ const middlewares = config.middlewares ?? chain();
9
+ const basePath = config.basePath?.replace(/\/?$/, '/');
10
+ const patterns = mapValues(config.routes, ({ path }) => basePath ? new RoutePattern(path.source.replace(/^\/?/, basePath)) : path);
11
+ const allowOrigins = config.cors?.allowOrigins?.map(origin => {
12
+ if (!origin.includes('//')) {
13
+ origin = `https://${origin}`;
14
+ }
15
+ if (origin.includes('*')) {
16
+ return new RegExp(`^${origin
17
+ .replace(/\./g, '\\.')
18
+ .replace(/\*:/g, '[^:]+:') // Wildcard protocol
19
+ .replace(/\*\./g, '([^/.]+\\.)?') // Wildcard subdomain
20
+ }$`);
21
+ }
22
+ return new ExactPattern(origin);
23
+ });
24
+ return (handlers) => middlewares.use(async function (context) {
25
+ const request = context.request;
26
+ const url = (context.url ??= new URL(request.url));
27
+ let method = request.method.toUpperCase();
28
+ let isPreflight = false;
29
+ if (method === 'OPTIONS') {
30
+ method = request.headers.get('Access-Control-Request-Method') ?? 'GET';
31
+ isPreflight = true;
32
+ }
33
+ for (let i = 0; i < keys.length; i++) {
34
+ const route = config.routes[keys[i]].methods[method];
35
+ if (!route) {
36
+ continue;
37
+ }
38
+ const match = patterns[keys[i]].match(url);
39
+ if (!match) {
40
+ continue;
41
+ }
42
+ const handler = handlers[keys[i]][method];
43
+ if (!handler) {
44
+ if (config.debug) {
45
+ throw new Error(`Handler not found for route: ${keys[i]} ${method}`);
46
+ }
47
+ continue;
48
+ }
49
+ if (isPreflight) {
50
+ const origin = request.headers.get('Origin');
51
+ if (allowOrigins &&
52
+ !(origin && allowOrigins.some(pattern => pattern.test(origin)))) {
53
+ return new Response(null, { status: 403 });
54
+ }
55
+ return new Response(null, {
56
+ headers: {
57
+ 'Access-Control-Allow-Origin': origin ?? '*',
58
+ 'Access-Control-Allow-Methods': method,
59
+ 'Access-Control-Allow-Headers': request.headers.get('Access-Control-Request-Headers') ?? '',
60
+ },
61
+ });
62
+ }
63
+ if (route.path) {
64
+ const error = parsePathParams(context, enableStringParsing(route.path), match.params);
65
+ if (error) {
66
+ return httpClientError(error, 'Invalid path parameter', config);
67
+ }
68
+ }
69
+ else {
70
+ context.path = match.params;
71
+ }
72
+ if (route.headers) {
73
+ const error = parseHeaders(context, enableStringParsing(route.headers));
74
+ if (error) {
75
+ return httpClientError(error, 'Invalid request headers', config);
76
+ }
77
+ }
78
+ if (route.query) {
79
+ const error = parseQueryString(context, enableStringParsing(route.query));
80
+ if (error) {
81
+ return httpClientError(error, 'Invalid query string', config);
82
+ }
83
+ }
84
+ if (route.body) {
85
+ const error = await parseRequestBody(context, route.body);
86
+ if (error) {
87
+ return httpClientError(error, 'Invalid request body', config);
88
+ }
89
+ }
90
+ const result = await handler(context);
91
+ if (result instanceof Response) {
92
+ return result;
93
+ }
94
+ return Response.json(result);
95
+ }
96
+ });
97
+ }
98
+ function httpClientError(error, message, config) {
99
+ return Response.json({
100
+ ...error,
101
+ message: config.debug ? `${message}: ${error.message}` : message,
102
+ }, { status: 400 });
103
+ }
104
+ function parsePathParams(context, schema, params) {
105
+ const result = schema.safeParse(params);
106
+ if (!result.success) {
107
+ return result.error;
108
+ }
109
+ context.path = result.data;
110
+ return null;
111
+ }
112
+ function parseHeaders(context, schema) {
113
+ const headers = Object.fromEntries(context.request.headers);
114
+ const result = schema.safeParse(headers);
115
+ if (!result.success) {
116
+ return result.error;
117
+ }
118
+ context.headers = result.data;
119
+ return null;
120
+ }
121
+ function parseQueryString(context, schema) {
122
+ const result = schema.safeParse(Object.fromEntries(context.url.searchParams));
123
+ if (!result.success) {
124
+ return result.error;
125
+ }
126
+ context.query = result.data;
127
+ return null;
128
+ }
129
+ async function parseRequestBody(context, schema) {
130
+ const result = await context.request.json().then(body => schema.safeParse(body), error => ({ success: false, error }));
131
+ if (!result.success) {
132
+ return result.error;
133
+ }
134
+ context.body = result.data;
135
+ return null;
136
+ }
137
+ const seen = new WeakMap();
138
+ /**
139
+ * Traverse object and array schemas, finding schemas that expect a number or
140
+ * boolean, and replace those schemas with a new schema that parses the input
141
+ * value as a number or boolean.
142
+ */
143
+ function enableStringParsing(schema) {
144
+ if (schema.type === 'number') {
145
+ return z.pipe(z.transform(Number), schema);
146
+ }
147
+ if (schema.type === 'boolean') {
148
+ return z.pipe(z.transform(toBooleanStrict), schema);
149
+ }
150
+ if (schema.type === 'object') {
151
+ const cached = seen.get(schema);
152
+ if (cached) {
153
+ return cached;
154
+ }
155
+ const modified = z.object(mapValues(schema.def.shape, enableStringParsing));
156
+ seen.set(schema, modified);
157
+ return modified;
158
+ }
159
+ if (schema.type === 'array') {
160
+ return z.array(enableStringParsing(schema.def.element));
161
+ }
162
+ return schema;
163
+ }
164
+ function toBooleanStrict(value) {
165
+ return value === 'true' || (value === 'false' ? false : value);
166
+ }
167
+ class ExactPattern {
168
+ value;
169
+ constructor(value) {
170
+ this.value = value;
171
+ }
172
+ test(input) {
173
+ return input === this.value;
174
+ }
175
+ }
@@ -0,0 +1,85 @@
1
+ import { Params, RoutePattern } from '@remix-run/route-pattern';
2
+ import * as z from 'zod/mini';
3
+ export type Promisable<T> = T | Promise<T>;
4
+ export type Unchecked<T> = {
5
+ __unchecked__: T;
6
+ };
7
+ export type QueryMethod = {
8
+ path?: z.ZodMiniObject<any>;
9
+ query?: z.ZodMiniObject<any>;
10
+ body?: never;
11
+ headers?: z.ZodMiniObject<any>;
12
+ response: Unchecked<any>;
13
+ };
14
+ export type MutationMethod = {
15
+ path?: z.ZodMiniObject<any>;
16
+ query?: never;
17
+ body: z.ZodMiniType<any, any>;
18
+ headers?: z.ZodMiniObject<any>;
19
+ response?: Unchecked<any>;
20
+ };
21
+ export type RouteMethods = {
22
+ GET?: QueryMethod;
23
+ POST?: MutationMethod;
24
+ PUT?: MutationMethod;
25
+ PATCH?: MutationMethod;
26
+ DELETE?: MutationMethod;
27
+ };
28
+ export type Routes = {
29
+ [key: string]: {
30
+ path: RoutePattern;
31
+ methods: RouteMethods;
32
+ };
33
+ };
34
+ declare class Any {
35
+ private isAny;
36
+ }
37
+ type PathArgs<T> = T extends {
38
+ path: infer TPath extends string;
39
+ } ? Params<TPath> extends infer TParams ? {} extends TParams ? {
40
+ path?: TParams;
41
+ } : {
42
+ path: TParams;
43
+ } : unknown : unknown;
44
+ type QueryArgs<T> = T extends QueryMethod & {
45
+ query: infer TQuery;
46
+ } ? {} extends z.infer<TQuery> ? {
47
+ query?: z.infer<TQuery>;
48
+ } : {
49
+ query: z.infer<TQuery>;
50
+ } : unknown;
51
+ type MutationArgs<T> = T extends MutationMethod & {
52
+ body: infer TBody;
53
+ } ? {} extends z.infer<TBody> ? {
54
+ body?: z.infer<TBody>;
55
+ } : {
56
+ body: z.infer<TBody>;
57
+ } : unknown;
58
+ export type RouteArgs<T extends QueryMethod | MutationMethod = any> = ([
59
+ T
60
+ ] extends [Any] ? {
61
+ query?: any;
62
+ body?: any;
63
+ path?: any;
64
+ } : QueryArgs<T> & MutationArgs<T> & PathArgs<T>) & Omit<RequestInit, 'method' | 'body' | 'headers'> & {
65
+ headers?: Record<string, string | undefined>;
66
+ };
67
+ export type RouteRequest<TResult = any> = {
68
+ route: QueryMethod | MutationMethod;
69
+ path: RoutePattern;
70
+ method: string;
71
+ args: RouteArgs;
72
+ $result: TResult;
73
+ };
74
+ export type RouteResponse<TResult = any> = Response & {
75
+ json(): Promise<TResult>;
76
+ };
77
+ export type InferRouteResponse<T extends QueryMethod | MutationMethod> = T extends {
78
+ response: Unchecked<infer TResponse>;
79
+ } ? TResponse : void;
80
+ export type RouteFunction<T extends QueryMethod | MutationMethod> = {
81
+ (args: RouteArgs<T>): RouteRequest<InferRouteResponse<T>>;
82
+ $args: RouteArgs<T>;
83
+ $response: InferRouteResponse<T>;
84
+ };
85
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,9 +1,40 @@
1
1
  {
2
2
  "name": "rouzer",
3
- "version": "0.0.0",
3
+ "version": "1.0.0-beta.10",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "import": "./dist/index.js"
9
+ }
10
+ },
11
+ "peerDependencies": {
12
+ "zod": ">=4"
13
+ },
4
14
  "devDependencies": {
5
15
  "@alloc/prettier-config": "^1.0.0",
6
16
  "@typescript/native-preview": "7.0.0-dev.20251208.1",
7
- "prettier": "^3.7.4"
17
+ "prettier": "^3.7.4",
18
+ "tsc-lint": "^0.1.9",
19
+ "typescript": "^5.9.3",
20
+ "zod": "^4.1.13"
21
+ },
22
+ "dependencies": {
23
+ "@hattip/core": "^0.0.49",
24
+ "@remix-run/route-pattern": "^0.15.3",
25
+ "alien-middleware": "^0.10.2"
26
+ },
27
+ "prettier": "@alloc/prettier-config",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/alloc/rouzer.git"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "!*.tsbuildinfo"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsgo -b tsconfig.json"
8
39
  }
9
- }
40
+ }
package/readme.md ADDED
@@ -0,0 +1,93 @@
1
+ # rouzer
2
+
3
+ Type-safe routes shared by your server and client, powered by `zod/mini` (input validation + transforms), `@remix-run/route-pattern` (URL matching), and `alien-middleware` (typed middleware chaining). The router output is intended to be used with `@hattip/core` adapters.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pnpm add rouzer zod
9
+ ```
10
+
11
+ Everything is imported directly from `rouzer`.
12
+
13
+ ## Define routes (shared)
14
+
15
+ ```ts
16
+ // routes.ts
17
+ import * as z from 'zod/mini'
18
+ import { $type, route } from 'rouzer'
19
+
20
+ export const helloRoute = route('hello/:name', {
21
+ GET: {
22
+ query: z.object({
23
+ excited: z.optional(z.boolean()),
24
+ }),
25
+ // The response is only type-checked at compile time.
26
+ response: $type<{ message: string }>(),
27
+ },
28
+ })
29
+
30
+ export const routes = { helloRoute }
31
+ ```
32
+
33
+ The following request parts can be validated with Zod:
34
+
35
+ - `path`
36
+ - `query`
37
+ - `body`
38
+ - `headers`
39
+
40
+ Zod validation happens on both the server and client.
41
+
42
+ ## Server router
43
+
44
+ ```ts
45
+ import { chain, createRouter } from 'rouzer'
46
+ import { routes } from './routes'
47
+
48
+ const middlewares = chain().use(ctx => {
49
+ // An example middleware. For more info, see https://github.com/alien-rpc/alien-middleware#readme
50
+ return {
51
+ db: postgres(ctx.env('POSTGRES_URL')),
52
+ }
53
+ })
54
+
55
+ export const handler = createRouter({
56
+ routes,
57
+ middlewares,
58
+ debug: process.env.NODE_ENV === 'development',
59
+ })({
60
+ helloRoute: {
61
+ GET(ctx) {
62
+ const message = `Hello, ${ctx.path.name}${ctx.query.excited ? '!' : '.'}`
63
+ return { message }
64
+ },
65
+ },
66
+ })
67
+ ```
68
+
69
+ ## Client wrapper
70
+
71
+ ```ts
72
+ import { createClient } from 'rouzer'
73
+ import { helloRoute } from './routes'
74
+
75
+ const client = createClient({ baseURL: '/api/' })
76
+
77
+ const { message } = await client.json(
78
+ helloRoute.GET({ path: { name: 'world' }, query: { excited: true } })
79
+ )
80
+
81
+ // If you want the Response object, use `client.request` instead.
82
+ const response = await client.request(
83
+ helloRoute.GET({ path: { name: 'world' } })
84
+ )
85
+
86
+ const { message } = await response.json()
87
+ ```
88
+
89
+ ## Add an endpoint
90
+
91
+ 1. Declare it in `routes.ts` with `route(...)` and `zod/mini` schemas.
92
+ 2. Implement the handler in your router assembly with `createRouter(…)({ ... })`.
93
+ 3. Call it from the client with the generated helper via `client.json` or `client.request`.