Typesafe, validated server actions with Zod, for Next.js.
This library has zod
as peer dependency, since we use it to create a schema and validate the input. Install with your preferred package manager.
$ npm install nza zod
Let's say we wanted to have a server action to create a user task. The first thing would be to identify the input we need.
import { z } from 'zod';
const taskInput = z.object({
title: z.string(),
description: z.string().optional(),
done: z.boolean()
})
Next, we can define our typesafe server action. Import createServerAction
to do so.
import { z } from 'zod';
import { createServerAction } from 'nza';
const taskInput = z.object({
title: z.string(),
description: z.string().optional(),
done: z.boolean()
})
const serverAction = createServerAction()
.input(taskInput)
.handler(async (input) => {
const task = await db.task.create({
data: input
})
return task;
})
Of course, the handler will already have the input typed and the output inferred.
Aside from the handler itself, you can abstract common pieces of code used on multiple server actions and use them as middleware. For example:
/**
* A common middleware would be to require user authentication. For example, the following
* would work when using `next-auth`. If you need some input, you can grab it. It will respect
* the schema provided previously.
*
* Notice that you can return an object with `locals`. These are accumulated through every middleware and then
* passed down to the server action handler.
*/
async function withAuth(){
const session = await getSession()
if(!session){
throw new Error('You must be authenticated')
}
return {
session
}
}
const actionRequireAuth = createServerAction()
.input(...)
.use(withAuth)
.handler(async (input, locals) => {
// locals contains the session key we returned before.
const user = locals.session.user;
...
})
The locals
parameter is shared across middlewares as well, and accumulated with each call. At first, it will be an empty object, then it will merge every locals
return from middlewares. You can store anything you obtain on middlewares and that might need either on the following middlewares or on the server action handler.
If you need the input of the action on the middleware, or a part of it, count on having it.
function checkAge(input: { age: number }) {
return {
validAge: input.age >= 21
}
}
const test = createServerAction()
.input(
z.object({
name: z.string(),
age: z.number(),
})
)
.use(checkAge)
.handler((input, locals) => {
// locals will now contain 'validAge'
console.log(locals.validAge);
});