Easily validate FormData
, URLSearchParams
and JSON data in your Fresh app
server-side or client-side!
Fresh Validation is built on top of Zod. For more information on different validation rules you can check Zod's documentation.
You can use Fresh Validation directly without any setup:
FormData
are natively support on the web platform but suffer from loosing data
typing as all data are plain string when sent through the wire.
By using validateFormData
, we can validate and cast back our data to the right
type.
// routes/login.tsx
/** @jsx h */
import { h } from "preact";
import {
error,
validateFormData,
z,
ZodIssue,
} from "https://deno.land/x/[email protected]/mod.ts";
import { Handlers } from "$fresh/server.ts";
import type { WithSession } from "https://deno.land/x/[email protected]/mod.ts";
export const handler: Handlers<{ errors: ZodIssue[] }, WithSession> = {
GET(req, ctx) {
// We use Fresh Session to flash errors and pass it down to the page
// more info: https://github.com/xstevenyung/fresh-session
const errors = ctx.state.session.flash("errors");
return ctx.render({ errors });
},
async POST(req, ctx) {
// We just need this to validate our FormData
const { validatedData, errors } = await validateFormData(req, {
username: z.string().min(2),
password: z.string().min(8),
});
// `errors` will be null if the validation is correct
if (errors) {
// we can deal with errors here
// we recommand using Fresh Session to pass errors between endpoints
// more info: https://github.com/xstevenyung/fresh-session
ctx.state.session.flash("errors", errors);
}
// here we get back the validated data casted to the right type
validatedData.username;
validatedData.password;
// For the sake of the example, we will redirect to the dashboard after a successful login
return new Response(null, {
status: 303,
headers: { Location: "/dashboard" },
});
},
};
export default function ({ data }) {
return (
<form method="post">
<label
for="username"
// We can display a specific class or style if there is any errors on a specific field
class={error(data.errors, "username") ? "invalid" : ""}
>
Username
</label>
<input id="username" name="username" />
{/* And we can use the `error` function to retrieve the right error to display to the user*/}
{!!error(data.errors, "username") && (
<p>{error(data.errors, "username")?.message}</p>
)}
<label for="password">Password</label>
<input id="password" name="password" />
{!!error(data.errors, "password") && (
<p>{error(data.errors, "password")?.message}</p>
)}
</form>
);
}
URLSearchParams
works the same as FormData
.
// routes/search.tsx
/** @jsx h */
import { h } from "preact";
import {
error,
validate,
z,
ZodIssue,
} from "https://deno.land/x/[email protected]/mod.ts";
import testShape from "@/shapes/test.ts";
import { Handlers } from "$fresh/server.ts";
import type { WithSession } from "https://deno.land/x/[email protected]/mod.ts";
export const handler: Handlers<{ errors: ZodIssue[] }, WithSession> = {
GET(req, ctx) {
// Validate search params
const { validatedData, errors } = await validateSearchParams(req, {
q: z.string().nullable(),
page: z.number().default(1),
});
if (errors) {
// We can deal with errors here but in our example, it's not necessary
}
// We can then use it here
validatedData.q;
validatedData.page;
return ctx.render({ validatedData });
},
};
export default function ({ data }) {
return (
<form method="get">
<input id="search" name="search" />
<a href="/search?page=2">Page 2</a>
</form>
);
}
validateJSON
will extract the req.json()
and validate against a
Zod schema
Note: We transform any date into a Date
instance to simplify validation with
Zod
// routes/login.tsx
/** @jsx h */
import { h } from "preact";
import {
validateJSON,
z,
} from "https://deno.land/x/[email protected]/mod.ts";
import { Handlers } from "$fresh/server.ts";
import Form from "../islands/LoginForm.tsx";
export const handler: Handlers = {
GET: (req, ctx) => {
return ctx.render({ errors });
},
async POST(req) {
const { validatedData, errors } = await validateJSON(req, {
username: z.string().min(2),
password: z.string().min(8),
});
if (errors) {
// We deal with errors here
return new Response(JSON.stringify(errors), {
status: 422,
headers: { "Content-Type": "application/json" },
});
}
// We can access validatedData here
validatedData.username;
validatedData.password;
return new Response(null, {
status: 204,
headers: { "Content-Type": "application/json" },
});
},
};
export default function ({ data }) {
return <Form />;
}
// islands/LoginForm.tsx
/** @jsx h */
import { h } from "preact";
import { useState } from "preact/hooks";
import {
error,
validateFormData,
z,
ZodIssue,
} from "https://deno.land/x/[email protected]/mod.ts";
export default function ({ data }) {
const [errors, setErrors] = useState<ZodIssue[]>([]);
return (
<form
method="post"
onSubmit={async (e) => {
e.preventDefault();
// We can even do client-side validation with the exact same code!
const { validatedData, errors } = await validateFormData(
new FormData(e.target),
{
username: z.string().min(2),
password: z.string().min(8),
},
);
if (errors) {
return setErrors(errors);
}
fetch("/json", {
method: "POST",
body: JSON.stringify(validatedData),
headers: { "Content-Type": "application/json" },
}).then(async (response) => {
// We handle server-side errors in case there is some
if (response.status === 422) {
const { errors } = await response.json();
return setErrors(errors);
}
//
});
}}
>
<label for="username">Username</label>
<input id="username" name="username" />
{!!error(errors, "username") && (
<p>{error(errors, "username")?.message}</p>
)}
<label for="password">Password</label>
<input id="password" name="password" />
{!!error(errors, "password") && (
<p>{error(errors, "password")?.message}</p>
)}
<button type="submit">Submit</button>
</form>
);
}