Next.js
/
Validations and Errors
Validation keeps your app safe
Error handling improves user experience
With Zod you define data rules easily
Catch mistakes early
import { z } from "zod";
const postSchema = z.object({
title: z.string().min(1, "Title is required"),
content: z.string().min(10, "Content must be at least 10 characters"),
tags: z.array(z.string()).min(1, "At least one tag is required"),
});
const data = { title: "", content: "Hi", tags: [] };
const result = postSchema.safeParse(data);
// Returns { success: false, error: { title: "Title is required", ... } }
>Imagine users submitting junk data—like an empty blog post title. Without validation, your app could crash or store nonsense. Validation acts like a filter, catching mistakes before they cause trouble.
Users hate cryptic errors like “500 Internal Server Error.” Good error handling gives them clear, helpful messages instead, like “Please add a title.” Picture an admin submitting a post with no content or a broken date. Today, we’ll fix that with validation and smart error handling in a blogging app.
Zod is a library that lets us set up a schema—a blueprint of what our data should look like. It’s simple but powerful, especially with TypeScript.
Instead of trusting users to send perfect data, Zod checks it upfront. If something’s wrong, it tells us exactly what and why.
Let’s say our blogging app needs a title, content, and tags for each post. Zod ensures they’re all there and valid. This code defines a schema for a blog post. When we test it with bad data—like an empty title—it fails and gives us clear error messages we can show users.
Here we set the rules: title must be a string with at least 1 character, content needs 10, and tags must have at least one entry. This protects our app from bad input.
safeParse checks the data without crashing—it’s perfect for server-side validation.
Next.js
/
Validations and Errors
Use in APIs and server actions
Return errors, not crashes
"use server";
import { z } from "zod";
const postSchema = z.object({
title: z.string().min(1, "Title is required"),
content: z.string().min(10, "Content must be at least 10 characters"),
tags: z.array(z.string()).min(1, "At least one tag is required"),
});
export async function createPost(formData: FormData) {
const data = {
title: formData.get("title") as string,
content: formData.get("content") as string,
tags: formData.get("tags")?.toString().split(",") || [],
};
const result = postSchema.safeParse(data);
if (!result.success) {
return { error: result.error.format() };
}
// Simulate saving to DB
console.log("Saving post:", result.data);
return { success: true };
}
Next.js lets us validate in API routes or server actions. Both run on the server, so we can trust the checks even if the client messes up.
If validation fails, we send back a structured error—like “Title is required”—instead of letting the app break. Do not trust the client, always validate on server as well!
This server action validates a new blog post from a form. It’s a full example you can test—just hook it to a form and try submitting bad data!
First, we define the ZOD schema.
We grab the form data and shape it into an object. This step mimics what an admin might send when creating a post.
If validation fails, we return the errors in a neat format. This lets the frontend display them nicely to the user.
Otherwise, we return a message that all went well.
Next.js
/
TanStack Query on the Client
import { useMutation } from "@tanstack/react-query";
async function createPost(data: { title: string; content: string; tags: string[] }) {
const res = await fetch("/api/posts", {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
export function PostForm() {
const mutation = useMutation({
mutationFn: createPost,
onError(e) {
alert(e.message);
setState(e.data)
}
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
mutation.mutate({
title: formData.get("title") as string,
content: formData.get("content") as string,
tags: formData.get("tags")?.toString().split(",") || [],
},);
};
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="Title" />
<textarea name="content" placeholder="Content" />
<input name="tags" placeholder="Tags (comma-separated)" />
<button type="submit">Submit</button>
{mutation.isError && <p>Error: {mutation.error.message}</p>}
</form>
);
}
With tanstack it is beneficial to handle queries and mutations with helper functions, where you can read the outcome of the query and mutation.
In this case, the createPost function reads the outcome ouf the API fetch, and if it not ok, for example the return status is bigger than 200, it throws a client-side error.
The mutation then can handle the incoming error in the onError handler.