Skip to content

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:

ts
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:

ts
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:

ts
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:

ts
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.