Skip to content

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.

tsx
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)
  })
})
1
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.

tsx
type State = z.infer<typeof stateSchema>
1

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.

tsx
const validateMiddlewareWithZod: Middleware<State> = ({ nextState }) =>
  stateSchema.parse(nextState)
1
2

Finally, we can use the middleware in our store:

tsx
const { useAppState, useSelector, store } = create<State>({
  state: {
    user: {
      name: 'John',
      age: 32
    }
  },
  middleware: [validateMiddlewareWithZod]
})
1
2
3
4
5
6
7
8
9

Example

See the full example here:

tsx
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>
    </>
  )
}
1
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

Released under the MIT License.