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

Commit 4e1a513

Browse files
raksivHomelessDinosaurdavemooreuws
authored
Add Zod guide demonstrating validation of messages for apis, queues (#708)
Co-authored-by: Ryan Cartwright <[email protected]> Co-authored-by: David Moore <[email protected]>
1 parent 976bbaa commit 4e1a513

File tree

2 files changed

+245
-0
lines changed

2 files changed

+245
-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: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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-24
10+
updated_at: 2025-03-24
11+
---
12+
13+
# A Unified Approach to validation across APIs, Queues & Jobs with Zod
14+
15+
Validation can help users enter data in the correct format when interacting with front-end applications. This prevents 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](https://zod.dev/), 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+
43+
This schema ensures that any user data passed into the application follows these strict rules.
44+
45+
## Validating data
46+
47+
Once the schema is defined, we can use `.safeParse()` to validate incoming data before using it in our application.
48+
49+
```ts
50+
const validUser = userSchema.safeParse({
51+
name: 'Alice',
52+
53+
age: 25,
54+
})
55+
```
56+
57+
### What happens here?
58+
59+
- The `safeParse()` method checks if the input matches the `userSchema`.
60+
- If validation **passes**, the `validUser` object will contain the parsed data.
61+
- If validation **fails**, Zod returns an object containing detailed error messages instead of throwing an exception.
62+
63+
<Note>
64+
Alternatively, use .parse(), which throws an error that can be handled
65+
appropriately.
66+
</Note>{' '}
67+
68+
## Applying validation to an API endpoint
69+
70+
Now, let’s use our schema apply server-side validation to an API.
71+
72+
The following API route:
73+
74+
1. Extracts the request body from the incoming request
75+
2. Validates the data against `userSchema`
76+
3. Returns a 400 error if validation fails
77+
78+
```ts
79+
import { api } from '@nitric/sdk'
80+
import { z } from 'zod'
81+
82+
const userSchema = z.object({
83+
name: z.string().min(2, 'Name must be at least 2 characters'),
84+
email: z.string().email('Invalid email format'),
85+
age: z.number().min(18, 'Must be at least 18 years old'),
86+
})
87+
88+
const usersApi = api('users')
89+
90+
usersApi.post('/', async (ctx) => {
91+
const result = userSchema.safeParse(ctx.req.json()) // Validate request
92+
if (result.success) {
93+
ctx.res.json({ message: 'User validated.', result })
94+
} else {
95+
ctx.res.status = 400
96+
ctx.res.json(result.error)
97+
}
98+
})
99+
```
100+
101+
### Why this matters:
102+
103+
- If the request body is **valid**, the API processes the request as usual.
104+
- If **invalid**, the response includes detailed validation errors, ensuring that only properly formatted data is accepted.
105+
106+
### Example invalid request:
107+
108+
```json
109+
{
110+
"name": "test",
111+
"email": "[email protected]",
112+
"age": 12
113+
}
114+
```
115+
116+
### Expected response:
117+
118+
```json
119+
{
120+
"issues": [
121+
{
122+
"code": "too_small",
123+
"minimum": 18,
124+
"type": "number",
125+
"inclusive": true,
126+
"exact": false,
127+
"message": "Must be at least 18 years old",
128+
"path": ["age"]
129+
}
130+
],
131+
"name": "ZodError"
132+
}
133+
```
134+
135+
APIs aren’t the only place validation matters. Cloud applications move data through multiple services—including message queues and scheduled tasks.
136+
137+
## Validating and processing messages in a queue
138+
139+
If we don’t validate messages before processing them, bad data can propagate throughout the system.
140+
141+
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.
142+
143+
### Defining the validation schema
144+
145+
```ts
146+
import { api, queue, schedule } from '@nitric/sdk'
147+
import { z } from 'zod'
148+
149+
const orderSchema = z.object({
150+
orderId: z.string().uuid(),
151+
amount: z.number().positive(),
152+
userId: z.string().min(1),
153+
})
154+
```
155+
156+
### Key Rules:
157+
158+
- `orderId` must be a valid UUID
159+
- `amount` must be a positive number
160+
- `userId` must be a string with at least one character
161+
162+
This schema ensures that invalid orders never enter the system.
163+
164+
## Applying validation in a queue handler
165+
166+
Next, we define an API that accepts orders and enqueues them only if they pass validation.
167+
168+
```ts
169+
const orderApi = api('orders')
170+
const ordersQueue = queue('orders').allow('dequeue', 'enqueue')
171+
172+
const orderSchema = z.object({
173+
orderId: z.string().uuid(),
174+
amount: z.number().positive(),
175+
userId: z.string().min(1),
176+
})
177+
178+
orderApi.post('/', async (ctx) => {
179+
const result = orderSchema.safeParse(ctx.req.json())
180+
if (result.success) {
181+
await ordersQueue.enqueue(result.data)
182+
ctx.res.json({
183+
message: `Adding order with id: ${result.data.orderId} to queue`,
184+
})
185+
} else {
186+
ctx.res.status = 400
187+
ctx.res.json(result.error)
188+
}
189+
})
190+
```
191+
192+
### Breakdown:
193+
194+
- The request body is validated against `orderSchema`
195+
- If the data is valid, it gets enqueued for processing
196+
- If invalid, a `400` error response is returned
197+
198+
This prevents bad data from ever reaching the queue.
199+
200+
## Processing the Queue
201+
202+
To ensure only valid data is processed, we apply validation again when dequeuing orders.
203+
204+
```ts
205+
schedule('process-transactions').every('5 minutes', async (ctx) => {
206+
console.log(`Processing at ${new Date().toLocaleString()}`)
207+
208+
const tasks = await ordersQueue.dequeue()
209+
210+
await Promise.all(
211+
tasks.map(async (task) => {
212+
const result = orderSchema.safeParse(task.payload)
213+
214+
if (result.success) {
215+
console.log(
216+
`Processing order ${result.data.orderId} for user ${result.data.userId}`,
217+
)
218+
await task.complete()
219+
} else {
220+
console.error(`Invalid order message:`, result.error)
221+
}
222+
}),
223+
)
224+
})
225+
```
226+
227+
### What This Does:
228+
229+
- Every **5 minutes**, the function processes all messages in the queue
230+
- Each message is validated before being processed
231+
- Invalid messages are logged and ignored to prevent errors in downstream services
232+
233+
## Conclusion
234+
235+
Validation isn't just about catching errors, it’s about building confidence in how data moves through an application.
236+
237+
By integrating Zod into a Nitric application, we:
238+
239+
- Ensure APIs only accept well-formed requests
240+
- Prevent malformed messages from breaking queue processing
241+
- Guarantee background jobs run with predictable data
242+
- Validate real-time connections before they interact with the system
243+
244+
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)