HumanOnlyWeb

🍁 writer of code. drinker of coffee. human on the web.

Validating Route Params Client-Side in Nuxt 4 with Zod

By HumanOnlyWeb

Catch invalid route parameters before they hit your backend. Here's how to validate them client-side using Zod and Nuxt's definePageMeta.

Background

Stripe has a great pattern for resource IDs. A customer ID could look something like cus_J5O3b4x3eZvKYlo2. The cus_ prefix tells you it's a customer, and the rest is a unique identifier.

Let's borrow this idea for our routes.

An invoice route could look like /invoices/inv_XyZ123AbCdEfGhIj, where inv_ indicates it's an invoice.

Setting Up the ID Generator

First, we need a utility to generate these prefixed IDs (we use nanoid for unique ID generation):

// shared/utils/id-generator.ts
import { customAlphabet } from "nanoid";

export const IDConfig = {
  alphabet: "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz",
  alphabetUpper: "ABCDEFGHJKLMNPQRSTUVWXYZ23456789",
  longIdLength: 16
};

export const prefixes = {
  user: "usr",
  invoice: "inv",
  // Add more prefixes as needed
} as const;

export type IDPrefix = keyof typeof prefixes;

export const nanoid = customAlphabet(IDConfig.alphabet, IDConfig.longIdLength);
export const generateRandomCode = customAlphabet(IDConfig.alphabetUpper, IDConfig.codeLength);
export const randomId = (prefix: IDPrefix): string =>
  `${prefixes[prefix]}_${nanoid()}`;

Creating a new invoice ID is simple:

const newInvoiceId = randomId("invoice"); // "inv_XyZ123AbCdEfGhIj"

The Problem

Every time a user hits /invoices/inv_XyZ123AbCdEfGhIj, we send a request to the backend, query the database, and return the invoice. If it doesn't exist, we return a 404.

But what if someone types a clearly invalid URL like /invoices/not-a-valid-id? We're still making that backend request just to get a 404.

Validating route parameters before hitting the backend prevents unnecessary requests, improves security by rejecting malformed input early, and gives your users immediate feedback on invalid URLs.

The Validation Schema

Create a validation schema utility at shared/utils/validation-schema.ts.

// shared/utils/validation-schema.ts
import { z } from "zod";
import { prefixes, IDConfig, type IDPrefix } from "./id-generator";

const ID_PATTERN_TAIL = `[${IDConfig.alphabet}]{${IDConfig.longIdLength}}$`;

export const idPatterns = Object.fromEntries(
  Object.entries(prefixes).map(([key, val]) => [
    key,
    new RegExp(`^${val}_${ID_PATTERN_TAIL}`),
  ])
) as Record<IDPrefix, RegExp>;

export const createIdSchema = (
  prefixKey: IDPrefix,
  customMessage?: string
) =>
  z
    .string()
    .regex(
      idPatterns[prefixKey],
      customMessage || `Invalid ${prefixKey} ID`
    );

Validating in the Page Component

Now use this schema in your page component with definePageMeta's validate function:

<!-- app/pages/invoices/[invoiceId]/index.vue -->
<script setup lang="ts">
import { createIdSchema } from "~/shared/utils/validation-schema";

definePageMeta({
  validate(route) {
    const { invoiceId } = route.params;
    const invoiceIdSchema = createIdSchema("invoice");
    const result = invoiceIdSchema.safeParse(invoiceId);

    if (!result.success) {
      console.error("Invalid invoice ID:", result.error);
      return false;
    }

    return true;
  },
});

const route = useRoute('invoices-invoiceId');
const { data: invoice, error, pending } = await useFetch(`/api/invoices/${route.params.invoiceId}`);

// ...rest of the page logic
</script>

When validate returns false, Nuxt automatically shows a 404 error page, no backend request needed!

Learn more about definePageMeta, and validate in the official Nuxt docs.

Conclusion

With this setup, malformed invoice IDs (usually from someone manually typing the URL) get caught immediately. The user sees a 404, and your backend never has to deal with the invalid request.

This same technique can be extended to validate query params too, just grab them from route.query instead of route.params.

All Comments 0

Be the first to comment.