Skip to content

Commit c7ba474

Browse files
committed
feat: integrate Zod for schema validation in Express routes and update documentation
1 parent b59df81 commit c7ba474

15 files changed

Lines changed: 356 additions & 50 deletions

File tree

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
1-
import { Express, Request, Response } from "express";
2-
import { IAdminForth } from "adminforth";
1+
import { Express, Response } from "express";
2+
import { IAdminForth, IAdminUserExpressRequest } from "adminforth";
3+
import * as z from "zod";
4+
35
export function initApi(app: Express, admin: IAdminForth) {
46
app.get(`${admin.config.baseUrl}/api/hello/`,
57

6-
// you can use data API to work with your database https://adminforth.dev/docs/tutorial/Customization/dataApi/
7-
async (req: Request, res: Response) => {
8-
// req.adminUser to get info about the admin users
9-
const allUsers = await admin.resource("adminuser").list([]);
10-
res.json({
11-
message: "List of admin users from AdminForth API",
12-
users: allUsers,
13-
});
14-
},
8+
admin.express.withSchema(
9+
{
10+
description: "Returns example data from a custom Express API together with the current authenticated AdminForth user.",
11+
response: z.object({
12+
message: z.string(),
13+
users: z.array(z.record(z.string(), z.unknown())),
14+
adminUser: z.record(z.string(), z.unknown()),
15+
}),
16+
},
1517

16-
// you can use admin.express.authorize to get info about the current user
17-
admin.express.authorize(
18-
async (req: Request, res: Response) => {
19-
res.json({ message: "Current adminuser from AdminForth API", adminUser: req.adminUser });
20-
}
18+
// you can use data API to work with your database https://adminforth.dev/docs/tutorial/Customization/dataApi/
19+
// and admin.express.authorize to inject req.adminUser
20+
admin.express.authorize(
21+
async (req: IAdminUserExpressRequest, res: Response) => {
22+
const allUsers = await admin.resource("adminuser").list([]);
23+
res.json({
24+
message: "Hello from AdminForth API!",
25+
users: allUsers,
26+
adminUser: req.adminUser,
27+
});
28+
}
29+
)
2130
)
2231
);
2332
}

adminforth/commands/createApp/templates/package.json.hbs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"dependencies": {
3636
"@dotenvx/dotenvx": "^1.34.0",
3737
"adminforth": "{{adminforthVersion}}",
38-
"express": "latest-4"
38+
"express": "latest-4",
39+
"zod": "^4.3.6"
3940
},
4041
"devDependencies": {
4142
"typescript": "5.4.5",

adminforth/documentation/blog/2025-04-10-how-to-translate-dynamic-strings/index.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,17 @@ export default {
4545
You might have this page and return it in your API for nuxt:
4646

4747
```ts
48+
import * as z from "zod";
49+
4850
app.get(`${admin.config.baseUrl}/api/get_page`,
51+
admin.express.withSchema(
52+
{
53+
description: 'Returns translated SEO metadata for the page specified by the pageUrl query parameter.',
54+
response: z.object({
55+
meta_title: z.string(),
56+
meta_desc: z.string(),
57+
}),
58+
},
4959
async (req:any, res: Response): Promise<void> => {
5060
const pageUrl = req.query.pageUrl;
5161
if (!pageUrl) {
@@ -62,18 +72,29 @@ app.get(`${admin.config.baseUrl}/api/get_page`,
6272
meta_desc: page.meta_desc,
6373
});
6474
}
65-
)
75+
)
6676
);
6777
```
6878

79+
Install and import Zod before using this pattern: `pnpm add zod` or `npm install zod`, then `import * as z from 'zod';`. `admin.express.withSchema(...)` will convert the Zod schema to OpenAPI for you.
80+
6981
Now you want to translate page meta title and meta description. You can do this by using `i18n` plugin for AdminForth.
7082

7183
```ts
7284
import { AdminForth } from "adminforth";
85+
import * as z from "zod";
7386

7487
export const SEO_PAGE_CATEGORY = "seo_page_config";
7588

7689
app.get(`${admin.config.baseUrl}/api/get_page`,\
90+
admin.express.withSchema(
91+
{
92+
description: 'Returns translated SEO metadata for the page specified by the pageUrl query parameter.',
93+
response: z.object({
94+
meta_title: z.string(),
95+
meta_desc: z.string(),
96+
}),
97+
},
7798
//diff-add
7899
admin.express.translatable(
79100
async (req:any, res: Response): Promise<void> => {
@@ -107,6 +128,7 @@ app.get(`${admin.config.baseUrl}/api/get_page`,\
107128
meta_desc,
108129
});
109130
}
131+
)
110132
//diff-add
111133
)
112134
);

adminforth/documentation/docs/tutorial/03-Customization/06-customPages.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -310,12 +310,21 @@ Open `index.ts` file and add the following code *BEFORE* `admin.express.serve(`
310310
311311
import type { IAdminUserExpressRequest } from 'adminforth';
312312
import express from 'express';
313+
import * as z from 'zod';
313314
314315
....
315316
316317
app.get(`${ADMIN_BASE_URL}/api/dashboard/`,
317-
admin.express.authorize(
318-
async (req:IAdminUserExpressRequest, res: express.Response) => {
318+
admin.express.withSchema(
319+
{
320+
description: 'Returns aggregated apartment metrics for the custom dashboard page.',
321+
response: z.object({
322+
apartsByDays: z.array(z.record(z.string(), z.unknown())),
323+
totalAparts: z.number(),
324+
}).catchall(z.unknown()),
325+
},
326+
admin.express.authorize(
327+
async (req:IAdminUserExpressRequest, res: express.Response) => {
319328
const days = req.body.days || 7;
320329
const apartsByDays = admin.resource('aparts').dataConnector.client.prepare(
321330
`SELECT
@@ -403,7 +412,8 @@ app.get(`${ADMIN_BASE_URL}/api/dashboard/`,
403412
totalUnlistedPrice,
404413
listedVsUnlistedPriceByDays,
405414
});
406-
}
415+
}
416+
)
407417
)
408418
);
409419
@@ -413,11 +423,15 @@ admin.discoverDatabases();
413423
414424
```
415425
426+
Install and import Zod before using this pattern: `pnpm add zod` or `npm install zod`, then `import * as z from 'zod';`. `admin.express.withSchema(...)` will convert the Zod schema to OpenAPI for you.
427+
416428
417429
> ☝️ Please note that we are using `admin.express.authorize` middleware to check if the user is logged in. If you want to make this endpoint public, you can remove this middleware. If user is not logged in, the request will return 401 Unauthorized status code, and protect our statistics from leak.
418430
419431
> ☝️ Moreover if you wrap your endpoint with `admin.express.authorize` middleware, you can access `req.adminUser` object in your endpoint to get the current user information.
420432
433+
> ☝️ Wrapping the route with `admin.express.withSchema(...)` registers it in `/api/v1/openapi.json` and `/api-docs`. Define custom routes before `admin.express.serve(app)` so AdminForth can pick them up.
434+
421435
> ☝️ AdminForth does not provide any facility to access data in database. You are free to use any ORM like Prisma, TypeORM, Sequelize,
422436
mongoose, or just use raw SQL queries against your tables.
423437

adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,21 @@ Also we have to add an Api to get percentages:
112112
```ts title="./index.ts"
113113
import type { IAdminUserExpressRequest } from 'adminforth';
114114
import express from 'express';
115+
import * as z from 'zod';
115116
116117
....
117118
118119
app.get(`${ADMIN_BASE_URL}/api/aparts-by-room-percentages/`,
119-
admin.express.authorize(
120-
async (req: IAdminUserExpressRequest, res: express.Response) => {
120+
admin.express.withSchema(
121+
{
122+
description: 'Returns apartment room-count percentages for the page injection chart.',
123+
response: z.array(z.object({
124+
rooms: z.number(),
125+
percentage: z.number(),
126+
})),
127+
},
128+
admin.express.authorize(
129+
async (req: IAdminUserExpressRequest, res: express.Response) => {
121130
const roomPercentages = await admin.resource('aparts').dataConnector.client.prepare(
122131
`SELECT
123132
number_of_rooms,
@@ -139,7 +148,8 @@ import express from 'express';
139148
})
140149
)
141150
);
142-
}
151+
}
152+
)
143153
)
144154
);
145155
@@ -148,8 +158,12 @@ import express from 'express';
148158
admin.express.serve(app)
149159
```
150160
161+
Install and import Zod before using this pattern: `pnpm add zod` or `npm install zod`, then `import * as z from 'zod';`. `admin.express.withSchema(...)` will convert the Zod schema to OpenAPI for you.
162+
151163
> ☝️ Please note that we are using [Frontend API](/docs/api/FrontendAPI/interfaces/FrontendAPIInterface/) `adminforth.list.updateFilter({field: 'number_of_rooms', operator: 'eq', value: selectedRoomsCount});` to set filter when we are located on apartments list page
152164
165+
> ☝️ The outer `admin.express.withSchema(...)` wrapper makes this custom Express route appear in `/api/v1/openapi.json` and `/api-docs`.
166+
153167
Here is how it looks:
154168
![alt text](<Page Injections.png>)
155169

adminforth/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@
104104
"rate-limiter-flexible": "^8.1.0",
105105
"recast": "^0.23.11",
106106
"ws": "^8.18.0",
107-
"yaml": "^2.8.2"
107+
"yaml": "^2.8.2",
108+
"zod": "^4.3.6"
108109
},
109110
"devDependencies": {
110111
"@semantic-release/exec": "^7.1.0",

adminforth/pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)