Untrusted input
A request can send any fields it likes. Pick the ones you want before they reach a Model.
Permitting fields
Only permit the fields you want from a request:
const permitted = Account.permit(await request.json(), "email", "handle");
const account = await Account.create(permitted);TIP
permit can also read FormData or URLSearchParams.
Permitted values are cast to their attribute’s types, so "42" becomes 42 for an integer. Models lists what each type accepts. A page number from a query string can go straight to paginate, which casts it too.
Beyond Rails
Rails’ params.permit only filters keys. d1-record’s also casts each value, and checks that each name is an attribute.
Using a schema library
If you validate requests with a schema library such as Zod or Valibot, pass the parsed result to the Model:
import { z } from "zod";
const SignUp = z.object({ email: z.string(), handle: z.string().min(3) });
const fields = SignUp.parse(await request.json());
const account = await Account.create(fields);Returning a 400
A value that can’t be cast throws a TypeError, and a body that isn’t JSON throws a SyntaxError. Return a 400 for either:
export async function updateAccount(account: Account, request: Request): Promise<Response> {
let permitted;
try {
permitted = Account.permit(await request.json(), "handle", "seats");
} catch (error) {
if (error instanceof SyntaxError || error instanceof TypeError) {
return new Response("Bad request", { status: 400 });
}
throw error;
}
await account.update(permitted);
return Response.json(account);
}Querying with request values
Check that a value is a string before you query with it. JSON can send null, and findBy({ token: null }) matches rows whose token is NULL:
export async function accept(request: Request): Promise<Response> {
const { token } = await request.json<{ token: string }>();
if (typeof token !== "string" || token === "") {
return new Response("Send the invitation's token", { status: 400 });
}
const invitation = await Invitation.findBy({ token });
if (invitation === null) return new Response("No such invitation", { status: 404 });
await invitation.update({ token: null, acceptedAt: new Date() });
return new Response(`Welcome, ${invitation.email}`);
}TIP
A value that can’t be cast matches nothing, so whereNot({ id: "abc" }) excludes nothing.