Using Zod validation and middleware
About ZOD
ZOD is a TypeScript-first schema declaration and validation library. We can use ZOD to define a schema for our state and use ZOD to validate all state updates - to make sure the state is always in a good shape and valid.
This is especially useful during development, because it helps us to find bugs early. If for example, a server response is not in the expected format, we can detect invalid state updates early and fix the bug.
https://github.com/colinhacks/zod
Step 1: Define a schema for the state
First we have to define a schema for our state. We can use ZOD for that.
import { Middleware, create } from '@restate/core'
import { connectDevTools } from '@restate/dev-tools'
import { z } from 'zod'
const stateSchema = z.object({
user: z.object({
name: z.string(),
age: z.number().min(0).max(150)
})
})
2
3
4
5
6
7
8
9
10
Step 2: Infer the state type
We can ZOD to infer the state type from the schema, so we can use it in the app and middleware. Note that you have to set in tsconfig.json strictNullChecks
to false
to make it work.
type State = z.infer<typeof stateSchema>
Step 3: Validation Middleware
We write a simple middleware that use the stateSchema
to validate the nextState
. stateSchema
throws an ZodError if the next state is invalid. And if a middleware throws an exception, the state update will be canceled.
const validateMiddlewareWithZod: Middleware<State> = ({ nextState }) =>
stateSchema.parse(nextState)
2
Finally, we can use the middleware in our store:
const { useAppState, useSelector, store } = create<State>({
state: {
user: {
name: 'John',
age: 32
}
},
middleware: [validateMiddlewareWithZod]
})
2
3
4
5
6
7
8
9
Example
See the full example here:
import { Grid } from '@mui/material'
import { Middleware, create } from '@restate/core'
import { z } from 'zod'
//
// Step 1: Define a schema for the state
//
const stateSchema = z.object({
user: z.object({
name: z.string(),
age: z.number().min(0).max(150)
})
})
//
// Step2: Infer the state type from the schema, so we can use it in the app and middleware
//
type State = z.infer<typeof stateSchema>
// Step3: Write a simple middleware that throws an ZodError if the next state is invalid.
// Throwing an exception will cancel the state update.
//
// Instead of throwing an error, you may also modify
// the `nextState` state here, if you want to.
//
const validateMiddlewareWithZod: Middleware<State> = ({ nextState }) => {
stateSchema.parse(nextState)
}
const { useAppState, useSelector, store } = create<State>({
state: {
user: {
name: 'John',
age: 32
}
},
middleware: [validateMiddlewareWithZod] // use the middleware
})
function Name() {
const name = useSelector((state) => state.user.name)
return <h1>Hello {name}!</h1>
}
function ChangeName() {
const [name, setName] = useAppState((state) => state.user.name)
return (
<>
<label>Name:</label>
<input value={name} onChange={(e) => setName(e.target.value)} />
</>
)
}
function ChangeAge() {
const [age, setAge] = useAppState((state) => state.user.age)
return (
<>
<label>Age:</label>
<input value={age} onChange={(e) => setAge(Number(e.target.value))} />
</>
)
}
export function HelloZodValidation() {
return (
<>
<Name />
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '0.5rem',
maxWidth: '200px'
}}
>
<ChangeName />
<ChangeAge />
</div>
</>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
Full example on https://stackblitz.com/edit/hello-restate