Skip to content
This repository was archived by the owner on May 20, 2025. It is now read-only.

Commit 96c984b

Browse files
committed
Add Zod guide
1 parent 6c5cfe6 commit 96c984b

File tree

2 files changed

+250
-0
lines changed

2 files changed

+250
-0
lines changed

dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ ctx
221221
reproducibility
222222
misconfigurations
223223
misconfiguration
224+
dequeuing
224225
DSL
225226
LM
226227
1B

docs/guides/nodejs/zod.mdx

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
---
2+
description: 'Server-side validation with Zod across APIs, Schedules and Queues'
3+
tags:
4+
- API
5+
- Realtime & Websockets
6+
languages:
7+
- typescript
8+
- javascript
9+
published_at: 2025-03-05
10+
updated_at: 2025-03-05
11+
---
12+
13+
# A Unified Approach to validation across APIs, Queues & Jobs with Zod
14+
15+
On frontend applications validation can help users enter data in the correct format this stops them from providing empty values, invalid emails, or other common input mistakes.
16+
17+
Once data leaves the browser we still need to ensure that the data remains valid:
18+
19+
- What happens when services communicate asynchronously?
20+
- How do we prevent bad data from breaking downstream systems?
21+
22+
Cloud applications don’t just receive requests from web forms. Data comes from APIs, message queues, scheduled tasks, or WebSockets—each introducing the risk of incomplete, malformed, or even malicious data.
23+
24+
Server-side validation is critical—not just for security, but for maintaining data integrity across the system.
25+
26+
With Zod, we can define strict validation rules once and enforce them across APIs, events, and background jobs.
27+
28+
## Defining a validation schema with Zod
29+
30+
A Zod schema acts as a contract for how data should look. The example below defines a `userSchema`, which establishes clear rules for incoming user data.
31+
32+
```ts
33+
import { z } from 'zod'
34+
35+
const userSchema = z.object({
36+
name: z.string().min(2, 'Name must be at least 2 characters'),
37+
email: z.string().email('Invalid email format'),
38+
age: z.number().min(18, 'Must be at least 18 years old'),
39+
})
40+
```
41+
42+
### Breaking it down:
43+
44+
- `name` must be a string and at least 2 characters long. If it's too short, an error message is returned.
45+
- `email` must follow a valid email format, and Zod will automatically enforce this validation.
46+
- `age` must be a number and at least 18. If the value is below this threshold, an error message is returned.
47+
48+
This schema ensures that any user data passed into the application follows these strict rules.
49+
50+
## Validating data
51+
52+
Once the schema is defined, we can use `.safeParse()` to validate incoming data before using it in our application.
53+
54+
```ts
55+
const validUser = userSchema.safeParse({
56+
name: 'Alice',
57+
58+
age: 25,
59+
})
60+
```
61+
62+
### What happens here?
63+
64+
- The `safeParse()` method checks if the input matches the `userSchema`.
65+
- If validation **passes**, the `validUser` object will contain the parsed data.
66+
- If validation **fails**, Zod returns an object containing detailed error messages instead of throwing an exception.
67+
68+
<Note>
69+
Alternatively, use .parse(), which throws an error that can be handled
70+
appropriately.
71+
</Note>{' '}
72+
73+
## Applying validation to an API endpoint
74+
75+
Now, let’s use our schema apply server-side validation to an API.
76+
77+
The following API route:
78+
79+
1. Extracts the request body from the incoming request
80+
2. Validates the data against `userSchema`
81+
3. Returns a 400 error if validation fails
82+
83+
```ts
84+
import { api } from '@nitric/sdk'
85+
import { z } from 'zod'
86+
87+
const userSchema = z.object({
88+
name: z.string().min(2, 'Name must be at least 2 characters'),
89+
email: z.string().email('Invalid email format'),
90+
age: z.number().min(18, 'Must be at least 18 years old'),
91+
})
92+
93+
const usersApi = api('users')
94+
95+
usersApi.post('/', async (ctx) => {
96+
const result = userSchema.safeParse(ctx.req.json()) // Validate request
97+
if (result.success) {
98+
ctx.res.json({ message: 'User validated.', result })
99+
} else {
100+
ctx.res.status = 400
101+
ctx.res.json(result.error)
102+
}
103+
})
104+
```
105+
106+
### Why this matters:
107+
108+
- If the request body is **valid**, the API processes the request as usual.
109+
- If **invalid**, the response includes detailed validation errors, ensuring that only properly formatted data is accepted.
110+
111+
### Example invalid request:
112+
113+
```json
114+
{
115+
"name": "test",
116+
"email": "[email protected]",
117+
"age": 12
118+
}
119+
```
120+
121+
### Expected response:
122+
123+
```json
124+
{
125+
"issues": [
126+
{
127+
"code": "too_small",
128+
"minimum": 18,
129+
"type": "number",
130+
"inclusive": true,
131+
"exact": false,
132+
"message": "Must be at least 18 years old",
133+
"path": ["age"]
134+
}
135+
],
136+
"name": "ZodError"
137+
}
138+
```
139+
140+
APIs aren’t the only place validation matters. Cloud applications move data through multiple services—including message queues and scheduled tasks.
141+
142+
## Validating and processing messages in a queue
143+
144+
If we don’t validate messages before processing them, bad data can propagate throughout the system.
145+
146+
Let’s say we have an `orders` queue where payment services publish transaction messages. To maintain integrity, we must validate each order before processing it.
147+
148+
### Defining the validation schema
149+
150+
```ts
151+
import { api, queue, schedule } from '@nitric/sdk'
152+
import { z } from 'zod'
153+
154+
const orderSchema = z.object({
155+
orderId: z.string().uuid(),
156+
amount: z.number().positive(),
157+
userId: z.string().min(1),
158+
})
159+
```
160+
161+
### Key Rules:
162+
163+
- `orderId` must be a valid UUID
164+
- `amount` must be a positive number
165+
- `userId` must be a string with at least one character
166+
167+
This schema ensures that invalid orders never enter the system.
168+
169+
## Applying validation in a queue handler
170+
171+
Next, we define an API that accepts orders and enqueues them only if they pass validation.
172+
173+
```ts
174+
const orderApi = api('orders')
175+
const ordersQueue = queue('orders').allow('dequeue', 'enqueue')
176+
177+
const orderSchema = z.object({
178+
orderId: z.string().uuid(),
179+
amount: z.number().positive(),
180+
userId: z.string().min(1),
181+
})
182+
183+
orderApi.post('/', async (ctx) => {
184+
const result = orderSchema.safeParse(ctx.req.json())
185+
if (result.success) {
186+
await ordersQueue.enqueue(result.data)
187+
ctx.res.json({
188+
message: `Adding order with id: ${result.data.orderId} to queue`,
189+
})
190+
} else {
191+
ctx.res.status = 400
192+
ctx.res.json(result.error)
193+
}
194+
})
195+
```
196+
197+
### Breakdown:
198+
199+
- The request body is validated against `orderSchema`
200+
- If the data is valid, it gets enqueued for processing
201+
- If invalid, a `400` error response is returned
202+
203+
This prevents bad data from ever reaching the queue.
204+
205+
## Processing the Queue
206+
207+
To ensure only valid data is processed, we apply validation again when dequeuing orders.
208+
209+
```ts
210+
schedule('process-transactions').every('5 minutes', async (ctx) => {
211+
console.log(`Processing at ${new Date().toLocaleString()}`)
212+
213+
const tasks = await ordersQueue.dequeue()
214+
215+
await Promise.all(
216+
tasks.map(async (task) => {
217+
const result = orderSchema.safeParse(task.payload)
218+
219+
if (result.success) {
220+
console.log(
221+
`Processing order ${result.data.orderId} for user ${result.data.userId}`,
222+
)
223+
await task.complete()
224+
} else {
225+
console.error(`Invalid order message:`, result.error)
226+
}
227+
}),
228+
)
229+
})
230+
```
231+
232+
### What This Does:
233+
234+
- Every **5 minutes**, the function processes all messages in the queue
235+
- Each message is validated before being processed
236+
- Invalid messages are logged and ignored to prevent errors in downstream services
237+
238+
## Conclusion
239+
240+
Validation isn't just about catching errors, it’s about building confidence in how data moves through an application.
241+
242+
By integrating Zod into a Nitric application, we:
243+
244+
- Ensure APIs only accept well-formed requests
245+
- Prevent malformed messages from breaking queue processing
246+
- Guarantee background jobs run with predictable data
247+
- Validate real-time connections before they interact with the system
248+
249+
Now that you've established server-side validation, the next step is handling errors effectively, some errors and failures will require retries, while others should be logged and flagged for manual review.

0 commit comments

Comments
 (0)