Skip to content

Commit 6e492bb

Browse files
authored
Support Valibot (#86)
* Add valibot * Add test and example for valibot * organize
1 parent 2daede4 commit 6e492bb

File tree

12 files changed

+1020
-104
lines changed

12 files changed

+1020
-104
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import express from "express";
2+
import { asAsync } from "../../../src/express";
3+
import { pathMap } from "./spec";
4+
import { ToHandlers, typed } from "../../../src/express/valibot";
5+
6+
const emptyMiddleware = (
7+
req: express.Request,
8+
res: express.Response,
9+
next: express.NextFunction,
10+
) => next();
11+
type Handlers = ToHandlers<typeof pathMap>;
12+
const newApp = () => {
13+
const app = express();
14+
app.use(express.json());
15+
// `typed` method is equivalent to below 2 lines code:
16+
// ```
17+
// // validatorMiddleware allows to use res.locals.validate method
18+
// app.use(validatorMiddleware(pathMap));
19+
// // wApp is same as app, but with additional common information
20+
// const wApp = app as TRouter<typeof pathMap>;
21+
// ```
22+
const wApp = asAsync(typed(pathMap, app));
23+
wApp.get("/users", emptyMiddleware, (req, res) => {
24+
{
25+
// @ts-expect-error params is not defined because pathMap["/users"]["get"].params is not defined
26+
res.locals.validate(req).params();
27+
}
28+
29+
// validate method is available in res.locals
30+
// validate(req).query() is equals to pathMap["/users"]["get"].query.safeParse(req.query)
31+
const { data, error } = res.locals.validate(req).query();
32+
if (data !== undefined) {
33+
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["get"].res["200"]
34+
res.status(200).json({ userNames: [`page${data.page}#user1`] });
35+
} else {
36+
// res.status(400).json() accepts only the response schema defined in pathMap["/users"]["get"].res["400"]
37+
res.status(400).json({ errorMessage: error.toString() });
38+
}
39+
});
40+
wApp.post("/users", (req, res) => {
41+
// validate(req).body() is equals to pathMap["/users"]["post"].body.safeParse(req.body)
42+
const { data, error } = res.locals.validate(req).body();
43+
{
44+
// Request header also can be validated
45+
res.locals.validate(req).headers();
46+
}
47+
if (data !== undefined) {
48+
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["post"].res["200"]
49+
res.status(200).json({ userId: data.userName + "#0" });
50+
} else {
51+
// res.status(400).json() accepts only the response schema defined in pathMap["/users"]["post"].res["400"]
52+
res.status(400).json({ errorMessage: error.toString() });
53+
}
54+
});
55+
56+
const getUserHandler: Handlers["/users/:userId"]["get"] = (req, res) => {
57+
const { data: params, error } = res.locals.validate(req).params();
58+
59+
if (params !== undefined) {
60+
// res.status(200).json() accepts only the response schema defined in pathMap["/users/:userId"]["get"].res["200"]
61+
res.status(200).json({ userName: "user#" + params.userId });
62+
} else {
63+
// res.status(400).json() accepts only the response schema defined in pathMap["/users/:userId"]["get"].res["400"]
64+
res.status(400).json({ errorMessage: error.toString() });
65+
}
66+
};
67+
wApp.get("/users/:userId", getUserHandler);
68+
69+
return app;
70+
};
71+
72+
const main = async () => {
73+
const app = newApp();
74+
const port = 3000;
75+
app.listen(port, () => {
76+
console.log(`Example app listening on port ${port}`);
77+
});
78+
};
79+
80+
main();
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { PathMap } from "./spec";
2-
import JSONT from "../../src/json";
3-
import { unreachable } from "../../src/utils";
4-
import type FetchT from "../../src/fetch";
2+
import JSONT from "../../../src/json";
3+
import { unreachable } from "../../../src/utils";
4+
import type FetchT from "../../../src/fetch";
55

66
const fetchT = fetch as FetchT<typeof origin, PathMap>;
77
const origin = "http://localhost:3000";

examples/express/valibot/spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import * as v from "valibot";
2+
import { ToApiEndpoints, ValibotApiEndpoints } from "../../../src/valibot";
3+
4+
const JsonHeader = v.object({
5+
"Content-Type": v.literal("application/json"),
6+
});
7+
export const pathMap = {
8+
"/users": {
9+
get: {
10+
query: v.object({
11+
page: v.string(),
12+
}),
13+
resBody: {
14+
200: v.object({ userNames: v.array(v.string()) }),
15+
400: v.object({ errorMessage: v.string() }),
16+
},
17+
},
18+
post: {
19+
headers: JsonHeader,
20+
resHeaders: JsonHeader,
21+
resBody: {
22+
200: v.object({ userId: v.string() }),
23+
400: v.object({ errorMessage: v.string() }),
24+
},
25+
body: v.object({
26+
userName: v.string(),
27+
}),
28+
},
29+
},
30+
"/users/:userId": {
31+
get: {
32+
params: v.object({ userId: v.string() }),
33+
resBody: {
34+
200: v.object({ userName: v.string() }),
35+
400: v.object({ errorMessage: v.string() }),
36+
},
37+
},
38+
},
39+
} satisfies ValibotApiEndpoints;
40+
export type PathMap = ToApiEndpoints<typeof pathMap>;
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import express from "express";
2-
import { asAsync } from "../../src/express";
2+
import { asAsync } from "../../../src/express";
33
import { pathMap } from "./spec";
4-
import { ToHandlers, typed } from "../../src/express/zod";
4+
import { ToHandlers, typed } from "../../../src/express/zod";
55

66
const emptyMiddleware = (
77
req: express.Request,

examples/express/zod/fetch.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type { PathMap } from "./spec";
2+
import JSONT from "../../../src/json";
3+
import { unreachable } from "../../../src/utils";
4+
import type FetchT from "../../../src/fetch";
5+
6+
const fetchT = fetch as FetchT<typeof origin, PathMap>;
7+
const origin = "http://localhost:3000";
8+
const headers = { "Content-Type": "application/json" } as const;
9+
const JSONT = JSON as JSONT;
10+
11+
const main = async () => {
12+
{
13+
const path = `${origin}/users?page=1`;
14+
const method = "get";
15+
const res = await fetchT(path, { method });
16+
switch (res.status) {
17+
case 200: {
18+
// r is the response schema defined in pathMap["/users"]["get"].res["200"]
19+
const r = await res.json();
20+
console.log(`${path}:${method} => ${r.userNames}`);
21+
break;
22+
}
23+
case 400: {
24+
// e is the response schema defined in pathMap["/users"]["get"].res["400"]
25+
const e = await res.json();
26+
console.log(`${path}:${method} => ${e.errorMessage}`);
27+
break;
28+
}
29+
default:
30+
unreachable(res);
31+
}
32+
}
33+
{
34+
// case-insensitive method example
35+
await fetchT(`${origin}/users?page=1`, { method: "GET" });
36+
}
37+
{
38+
// query parameter example
39+
// TODO: Add common information for query parameter
40+
const path = `${origin}/users?page=1`;
41+
const method = "get";
42+
const res = await fetchT(path, { method });
43+
if (res.ok) {
44+
// r is the response schema defined in pathMap["/users"]["get"].res["20X"]
45+
const r = await res.json();
46+
console.log(`${path}:${method} => ${r.userNames}`);
47+
} else {
48+
// e is the response schema defined in pathMap["/users"]["get"].res other than "20X"
49+
const e = await res.json();
50+
console.log(`${path}:${method} => ${e.errorMessage}`);
51+
}
52+
}
53+
54+
{
55+
const path = `${origin}/users`;
56+
const method = "post";
57+
const res = await fetchT(path, {
58+
method,
59+
headers,
60+
// body is the request schema defined in pathMap["/users"]["post"].body
61+
// stringify is same as JSON.stringify but with common information
62+
body: JSONT.stringify({ userName: "user1" }),
63+
});
64+
if (res.ok) {
65+
// r is the response schema defined in pathMap["/users"]["post"].res["20X"]
66+
const r = await res.json();
67+
console.log(`${path}:${method} => ${r.userId}`);
68+
} else {
69+
// e is the response schema defined in pathMap["/users"]["post"].res other than "20X"
70+
const e = await res.json();
71+
console.log(`${path}:${method} => ${e.errorMessage}`);
72+
}
73+
}
74+
75+
{
76+
// path parameter example
77+
// "/users/:userId" accepts `/users/${string}` pattern
78+
const path = `${origin}/users/1`;
79+
const method = "get";
80+
const res = await fetchT(path, { method });
81+
if (res.ok) {
82+
// r is the response schema defined in pathMap["/users/:userId"]["get"].res["20X"]
83+
const r = await res.json();
84+
console.log(`${path}:${method} => ${r.userName}`);
85+
} else {
86+
// e is the response schema defined in pathMap["/users/:userId"]["get"].res other than "20X"
87+
const e = await res.json();
88+
console.log(`${path}:${method} => ${e.errorMessage}`);
89+
}
90+
}
91+
};
92+
93+
main();
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { z } from "zod";
2-
import { ToApiEndpoints, ZodApiEndpoints } from "../../src";
2+
import { ToApiEndpoints, ZodApiEndpoints } from "../../../src";
33

44
const JsonHeader = z.object({
55
"Content-Type": z.literal("application/json"),

0 commit comments

Comments
 (0)