Data Validators

Data validators in Qwik Router are essential for validating request events and data for actions and loaders. These validations occur on the server-side before the execution of the associated action or loader. Similar to the zod$() function, Qwik provides a dedicated validator$() function for this purpose.

import {
  type RequestEvent,
  type RequestEventAction,
  routeAction$,
  validator$,
} from "@qwik.dev/router";
 
export const useAction = routeAction$(
  async (data, requestEvent: RequestEventAction) => {
    return { foo: "bar" };
  },
  validator$(async (ev: RequestEvent, data) => {
    if (ev.query.get("secret") === "123") {
      return { success: true };
    }
    return {
      success: false,
      error: {
        message: "secret is not correct",
      },
    };
  }),
);

When submitting a request to a routeAction(), the request event and data undergo validation against the defined validator. If the validation fails, the action surfaces the validation error on the routeAction.error property (a ServerError), separate from the success value on routeAction.value.

export default component$(() => {
  const action = useAction();
 
  // both are undefined before submitting; only one is set after
  if (action.error) {
    // validation failed if query string has no secret
    // action.error is a ServerError: .status is the HTTP status, and the
    // validator error fields are spread directly onto the error, so you read
    // them flat (e.g. action.error.message). The canonical payload object is
    // still available as action.error.data.
    action.error.message satisfies string;
    action.error.data satisfies { message: string };
  } else if (action.value) {
    action.value satisfies { searchResult: string };
  }
 
  return (
    <button onClick$={() => action.submit({ search: "foo" })}>Submit</button>
  );
});

Multiple validators

Actions and loaders can have multiple validators, which are executed in reverse order. In the following example, validators execute in the order validator3 -> validator2 -> validator1.

const validator1 = validator$(/*...*/)
const validator2 = validator$(/*...*/)
const validator3 = validator$(/*...*/)
 
export const useAction = routeAction$(
  async (data, requestEvent: RequestEventAction) => {
    return { foo: "bar" };
  },
  validator1,
  validator2,
  validator3, // will be executed first
);

If validator3 has a data property in its success return object, this data will be passed to the next validator, validator2. If you don't want to override the original submitted data, avoid putting the data property in the success return object.

export const useAction = routeAction$(
  async (data, requestEvent: RequestEventAction) => {
    console.log(data); // { message: "hi, I am validator1" }
    return { foo: "bar" };
  },
  // validator1
  validator$((ev, data) => {
    console.log(data); // { message: "hi, I am validator2" }
    return {
      success: true,
      data: {
        message: "hi, I am validator1",
      },
    };
  }),
  // validator2
  validator$((ev, data) => {
    console.log(data); // { message: "hi, I am validator3" }
    return {
      success: true,
      data: {
        message: "hi, I am validator2",
      },
    };
  }),
  // validator3
  validator$((ev, data) => {
    console.log(data); // Your submitted data
    return {
      success: true,
      data: {
        message: "hi, I am validator3",
      },
    };
  }),
);

Return object

Data validators expect specific properties in their return objects.

Successful validation

The success property must be true for a successful validation.

interface Success {
  success: true;
  data?: any;
}

Failed validation

interface Fail {
  success: false;
  error: Record<string, any>;
  status?: number;
}

When a validator returns a failed object, the framework turns it into a ServerError — the same outcome as return fail(status, data) inside the action or loader. The error surfaces on action.error, with action.error.status set to the validator's status, and the page keeps rendering (the failure is meant to be displayed inline; action.value stays undefined). The validator's error object fields are spread directly onto the error, so you read them flat (e.g. action.error.message); the canonical payload object is also available as action.error.data.

const status = 500;
const errorData = { message: "123" };
 
export const useAction = routeAction$(
  async (_, { fail }) => {
    return fail(status, errorData);
  },
  validator$(async () => {
    return {
      success: false,
      status,
      error: errorData,
    };
  }),
);

To abort the request and render the error page instead of surfacing an inline failure, throw error(status, data) from the action or loader body — a thrown error() never lands on action.error.

Use validator$() with zod$() together in actions

For actions, the typed data validator zod$() should be the second argument of routeAction$, followed by other data validators validator$()s.

export const useAction = routeAction$(
  async (data, requestEvent: RequestEventAction) => {
    return { foo: "bar" };
  },
  zod$(/*...*/),
  validator$(/*...*/),
  validator$(/*...*/),
);

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • wtlin1228
  • harishkrishnan24