Zod utilities for Remix loaders and actions.

Overview

Zodix

Zodix is a collection of Zod utilities for Remix loaders and actions. It abstracts the complexity of parsing and validating FormData and URLSearchParams so your loaders/actions stay clean and are strongly typed.

Remix loaders often look like:

export async function loader({ params, request }: LoaderArgs) {
  const { id } = params;
  const url = new URL(request.url);
  const count = url.searchParams.get('count') || '10';
  if (typeof id !== 'string') {
    throw new Error('id must be a string');
  }
  const countNumber = parseInt(count, 10);
  if (isNaN(countNumber)) {
    throw new Error('count must be a number');
  }
  // Fetch data with id and countNumber
};

Here is the same loader with Zodix:

export async function loader({ params, request }: LoaderArgs) {
  const { id } = zx.parseParams(params, { id: z.string() });
  const { count } = zx.parseQuery(request, { count: zx.NumAsString });
  // Fetch data with id and countNumber
};

Highlights

  • Significantly reduce Remix action/loader bloat
  • Avoid the oddities of FormData and URLSearchParams.
  • Tiny with no external dependencies. Less than 1kb gzipped.
  • Use existing Zod schemas, or write them on the fly.
  • Custom Zod schemas for stringified numbers, booleans, and checkboxes.
  • Full unit test coverage

Install

npm install zodix zod

Usage

Import

import { zx } from 'zodix';

// Or

import { parseParams, NumAsString } from 'zodix';

zx.parseParams(params: Params, schema: Schema)

Parse and validate the Params object from LoaderArgs['params'] or ActionArgs['params'] using a Zod shape:

export async function loader({ params }: LoaderArgs) {
  const { userId, noteId } = zx.parseParams(params, {
    userId: z.string(),
    noteId: z.string(),
  });
};

The same as above, but using an existing Zod object schema:

// This is if you have many pages that share the same params.
export const ParamsSchema = z.object({ userId: z.string(), noteId: z.string() });

export async function loader({ params }: LoaderArgs) {
  const { userId, noteId } = zx.parseParams(params, ParamsSchema);
};

zx.parseForm(request: Request, schema: Schema)

Parse and validate FormData from a Request in a Remix action and avoid the tedious FormData dance:

export async function action({ request }: ActionArgs) {
  const { email, password, saveSession } = await zx.parseForm(request, {
    email: z.string().email(),
    password: z.string().min(6),
    saveSession: zx.CheckboxAsString,
  });
};

Integrate with existing Zod schemas and models/controllers:

// db.ts
export const CreatNoteSchema = z.object({
  userId: z.string(),
  title: z.string(),
  category: NoteCategorySchema.optional(),
});

export function createNote(note: z.infer<typeof CreateNoteSchema>) {}
import { CreateNoteSchema, createNote } from './db';

export async function action({ request }: ActionArgs) {
  const formData = await zx.parseForm(request, CreateNoteSchema);
  createNote(formData); // No TypeScript errors here
};

zx.parseQuery(request: Request, schema: Schema)

Parse and validate the query string (search params) of a Request:

export async function loader({ request }: LoaderArgs) {
  const { count, page } = zx.parseQuery(request, {
    // NumAsString parses a string number ("5") and returns a number (5)
    count: zx.NumAsString,
    page: zx.NumAsString,
  });
};

Helper Zod Schemas

Because FormData and URLSearchParams serialize all values to strings, you often end up with things like "5", "on" and "true". The helper schemas handle parsing and validating strings representing other data types and are meant to be used with the parse functions.

Available Helpers

zx.BoolAsString

  • "true"true
  • "false"false
  • "notboolean" → throws ZodError

zx.CheckboxAsString

  • "on"true
  • undefinedfalse
  • "anythingbuton" → throws ZodError

zx.IntAsString

  • "3"3
  • "3.14" → throws ZodError
  • "notanumber" → throws ZodError

zx.NumAsString

  • "3"3
  • "3.14"3.14
  • "notanumber" → throws ZodError

See the tests for more details.

Usage

const Schema = z.object({
  isAdmin: zx.BoolAsString,
  agreedToTerms: zx.CheckboxAsString,
  age: zx.IntAsString,
  cost: zx.NumAsString,
});

const parsed = Schema.parse({
  isAdmin: 'true',
  agreedToTerms: 'on',
  age: '38',
  cost: '10.99'
});

/*
parsed = {
  isAdmin: true,
  agreedToTerms: true,
  age: 38,
  cost: 10.99
}
*/

Extras

Custom URLSearchParams parsing

You may have URLs with query string that look like ?ids[]=1&ids[]=2 or ?ids=1,2 that aren't handled as desired by the built in URLSearchParams parsing.

You can pass a custom function, or use a library like query-string to parse them with Zodix.

// Create a custom parser function
type ParserFunction = (params: URLSearchParams) => Record<string, string | string[]>;
const customParser: ParserFunction = () => { /* ... */ };

// Parse non-standard search params
const search = new URLSearchParams(`?ids[]=id1&ids[]=id2`);
const { ids } = zx.parseQuery(
  request,
  { ids: z.array(z.string()) }
  { parser: customParser }
);

// ids = ['id1', 'id2']

Actions with Multiple Intents

Zod discriminated unions are great for helping with actions that handle multiple intents like this:

// This adds type narrowing by the intent property
const Schema = z.discriminatedUnion('intent', [
  z.object({ intent: z.literal('delete'), id: z.string() }),
  z.object({ intent: z.literal('create'), name: z.string() }),
]);

export async function action({ request }: ActionArgs) {
  const data = await zx.parseForm(request, Schema);
  switch (data.intent) {
    case 'delete':
      // data is now narrowed to { intent: 'delete', id: string }
      return;
    case 'create':
      // data is now narrowed to { intent: 'create', name: string }
      return;
    default:
      // data is now narrowed to never. This will error if a case is missing.
      const _exhaustiveCheck: never = data;
  }
};
Comments
  • feat: Support asynchronous schema parsing

    feat: Support asynchronous schema parsing

    Here is my proposal to support asynchronous schema parsing.

    ⚠️ This proposal introduces breaking changes, i.e. all zx.parseXXX methods are now asynchronous.

    @rileytomasek Let me know if you're okay with this change or if you'd rather add asynchronous methods to zodix.

    Fixes #19

    opened by mdoury 14
  • Fix issue with Locking

    Fix issue with Locking

    Was using the package along with another that needed access to the request form data and was getting an error that the request was being locked, and wouldn't be able to complete the request because of this.

    Made changes where it will now clone the request and do the parsing on that request instead of the actual request to then forward it onto other packages.

    opened by shortnd 5
  • feat: Support FormData object entries parsing

    feat: Support FormData object entries parsing

    Here is a solution to support object entries in FormData which enables <input type="file" /> parsing.

    FormData object entries are stringified before FormData is parsed using the URLSearchParams trick.

    FormData object entries are not parsed by the default parser, making it rather versatile as it enables users to parse object entries of any type using a custom parser.

    I had to make some typings generic in order to support custom parsed search params types.

    Fixes #14.

    opened by mdoury 5
  • [BUG] TypeError: keyValidator._parse is not a function

    [BUG] TypeError: keyValidator._parse is not a function

    using separate zod schema doen't work

    🚀 ~ action ~ error TypeError: keyValidator._parse is not a function
        at ZodObject._parse (types.js:1188:37)
        at ZodObject._parseSync (types.js:109:29)
        at ZodObject.safeParse (types.js:139:29)
        at ZodObject.parse (types.js:120:29)
        at Object.parseForm (index.js:50:22)
        at async action (main.tsx:25:20)
        at async callLoaderOrAction (router.ts:2530:14)
        at async handleAction (router.ts:981:16)
        at async startNavigation (router.ts:904:26)
        at async Object.navigate (router.ts:784:12)
    

    here the repo for reproduce: zodix-rr-reproduce

    opened by iamyuu 5
  • Ability to parse array from FormData

    Ability to parse array from FormData

    The docs include an example on how to parse multiple SearchParams with the same name, but there doesn't appear to be support for parsing an array from formData entries with the same name.

    opened by devbytyler 5
  • Unable to parse file uploads handled with `unstable_createFileUploadHandler`

    Unable to parse file uploads handled with `unstable_createFileUploadHandler`

    I'm trying to make file uploads type safe using zx.parseFormSafe but I'm not able to parse the NodeOnDiskFile class.

    import type { ActionArgs } from "@remix-run/node";
    import {
      json,
      NodeOnDiskFile,
      unstable_composeUploadHandlers,
      unstable_createFileUploadHandler,
      unstable_createMemoryUploadHandler,
      unstable_parseMultipartFormData,
    } from "@remix-run/node";
    import type { ZodError } from "zod";
    import { z } from "zod";
    import { zx } from "zodix";
    
    function errorAtPath(error: ZodError, path: string) {
      return error.issues.find((issue) => issue.path[0] === path)?.message;
    }
    
    export async function action({ request }: ActionArgs) {
      const uploadHandler = unstable_composeUploadHandlers(
        unstable_createFileUploadHandler({
          maxPartSize: 5_000_000,
          file: ({ filename }) => filename,
        }),
        // parse everything else into memory
        unstable_createMemoryUploadHandler()
      );
      const formData = await unstable_parseMultipartFormData(
        request,
        uploadHandler
      );
    
      const results = await zx.parseFormSafe(formData, {
        file: z.instanceof(NodeOnDiskFile),
        // And some more fields that work just fine 👌
      });
    
      if (results.success) {
        return await doSomethingWithImage(results.data);
      } else {
        return json({
          success: results.success,
          error: errorAtPath(results.error, "image"),
        });
      }
    }
    

    formData.get("image") instanceof NodeOnDiskFile actually evaluates to true.

    I found a rather similar issue on the zod repo where running z.instanceof(File) on the server resulted in a Reference Error because the File class is not defined in Node, which is not the case of NodeOnDiskFile.

    I tried another method to parse the data which would result in an any type inference but would at least throw for missing data :

    const results = await zx.parseFormSafe(formData, {
        image: z.any().refine((file: unknown) => {
          console.log({ file }); 
          return Boolean(file);
        }, "Image is required."),
      });
    

    Which logged { file: '[object File]' } suggesting the file is casted to a string before it reaches the refinement function.

    I don't know if this issue comes from zod or from zodix but maybe someone encountered the issue before and found a workaround.

    Thanks!

    opened by mdoury 2
  • Add `.parseAction()` and `.parseLoader()`

    Add `.parseAction()` and `.parseLoader()`

    These could combine the parsing of loaders/actions that use both params and query. Could look something like:

    export async function loader(args: LoaderArgs) {
      const { id, count } = zx.parseLoader(args, {
        params: { id: z.string() },
        query: { count: zx.NumAsString },
      });
    };
    
    opened by rileytomasek 2
  • Support asynchronous parsing

    Support asynchronous parsing

    opened by mdoury 1
  • Infer type of `ZodEffects` schemas using `zx.parseFormSafe`

    Infer type of `ZodEffects` schemas using `zx.parseFormSafe`

    zx.parseFormSafe is able to parse ZodEffects schemas but it fails to infer the types. I added a failing test to reproduce issue #17 but I wasn't able to fix it instantly so I will leave it there for now. I will look into it next week probably.

    @rileytomasek I did not dig into zod typings so far, do you have any idea how to handle this?

    EDIT: I found a way to fix the type inference of result.error when user is parsing a ZodEffects schema with zx.parsFormSafe.

    Fixes #17

    opened by mdoury 1
  • `zx.parseFormSafe` type inference does not handle `ZodEffects`

    `zx.parseFormSafe` type inference does not handle `ZodEffects`

    I noticed the zx.parseFormSafe type inference does not handle well ZodEffects schemas resulting from the use of refine, superRefine or transform. I will create a PR with a failing test and go from there. Do you have any idea how to fix this?

    opened by mdoury 1
  • Support other runtimes

    Support other runtimes

    Right now, the package uses @remix-run/node to get the LoaderArgs type (and if you accept #1 to get the FormData class).

    This creates a hard requirement on Node as the runtime, but a Remix app could use Cloudflare or Deno as runtimes.

    You can solve this by importing from @remix-run/server-runtime. The LoaderArgs type is exported from there and re-exported from the runtime packages, and this way you avoid the dependency on a runtime.

    opened by sergiodxa 1
Releases(v0.4.0)
  • v0.4.0(Dec 11, 2022)

    What's Changed

    • feat: Support asynchronous schema parsing by @mdoury in https://github.com/rileytomasek/zodix/pull/20

    New Contributors

    • @ProdByGR made their first contribution in https://github.com/rileytomasek/zodix/pull/26

    Full Changelog: https://github.com/rileytomasek/zodix/compare/v0.3.2...v0.4.0

    Source code(tar.gz)
    Source code(zip)
  • v0.3.2(Nov 17, 2022)

    What's Changed

    • Infer type of ZodEffects schemas using zx.parseFormSafe by @mdoury in https://github.com/rileytomasek/zodix/pull/18
    • Fix issue with Locking by @shortnd in https://github.com/rileytomasek/zodix/pull/16

    New Contributors

    • @shortnd made their first contribution in https://github.com/rileytomasek/zodix/pull/16

    Full Changelog: https://github.com/rileytomasek/zodix/compare/v0.3.1...v0.3.2

    Source code(tar.gz)
    Source code(zip)
  • v0.3.1(Nov 9, 2022)

    What's Changed

    • feat: Support FormData object entries parsing by @mdoury in https://github.com/rileytomasek/zodix/pull/15

    New Contributors

    • @mdoury made their first contribution in https://github.com/rileytomasek/zodix/pull/15

    Full Changelog: https://github.com/rileytomasek/zodix/compare/v0.3.0...v0.3.1

    Source code(tar.gz)
    Source code(zip)
  • v0.3.0(Nov 5, 2022)

    This release makes it easier to handle errors with Zodix.

    Potentially Breaking Changes

    • parseParams()/parseForm()/parseQuery() now throw a Response object with a 400 status and statusMessage instead of a ZodError. This works better with the standard Remix flow for CatchBoundary. If you were catching and using the ZodErrors, check out the new safe parse functions.

    New

    • Added parseParamsSafe(), parseFormSafe(), and parseQuerySafe(). These functions don't throw when parsing fails and are meant for custom error handling situations like forms with user input.
    • Added an example Remix app with full examples of common usage patterns

    See the error handling documentation for more details.

    Full Changelog: https://github.com/rileytomasek/zodix/compare/v0.2.0...v0.3.0

    Source code(tar.gz)
    Source code(zip)
  • v0.2.0(Oct 14, 2022)

    What's Changed

    • Support passing FormData to zx.parseForm by @sergiodxa in https://github.com/rileytomasek/zodix/pull/1
    • Remove extra parenthesis in README by @tgdn in https://github.com/rileytomasek/zodix/pull/3
    • Support all Remix runtimes by @rileytomasek in https://github.com/rileytomasek/zodix/pull/4
    • Setup GitHub Actions by @rileytomasek in https://github.com/rileytomasek/zodix/pull/9

    New Contributors

    • @sergiodxa made their first contribution in https://github.com/rileytomasek/zodix/pull/1
    • @tgdn made their first contribution in https://github.com/rileytomasek/zodix/pull/3
    • @rileytomasek made their first contribution in https://github.com/rileytomasek/zodix/pull/4

    Full Changelog: https://github.com/rileytomasek/zodix/compare/v0.1.0...v0.2.0

    Source code(tar.gz)
    Source code(zip)
Owner
Riley Tomasek
Cofounder of hackers.dev and Standard Resume.
Riley Tomasek
Automatically document all of your Remix loaders and actions typings per each route. 📚

About remix-docs-gen parses all of your Remix loaders and actions and automatically documents all the typings per each route. Installation First, you

Stratulat Alexandru 50 Nov 9, 2022
Keep your Business Logic appart from your actions/loaders plumbing

Remix Domains Remix Domains helps you to keep your Business Logic appart from your actions/loaders plumbing. It does this by enforcing the parameters'

Seasoned 290 Jan 2, 2023
Grupprojekt för kurserna 'Javascript med Ramverk' och 'Agil Utveckling'

JavaScript-med-Ramverk-Laboration-3 Grupprojektet för kurserna Javascript med Ramverk och Agil Utveckling. Utvecklingsguide För information om hur utv

Svante Jonsson IT-Högskolan 3 May 18, 2022
Hemsida för personer i Sverige som kan och vill erbjuda boende till människor på flykt

Getting Started with Create React App This project was bootstrapped with Create React App. Available Scripts In the project directory, you can run: np

null 4 May 3, 2022
Kurs-repo för kursen Webbserver och Databaser

Webbserver och databaser This repository is meant for CME students to access exercises and codealongs that happen throughout the course. I hope you wi

null 14 Jan 3, 2023
Deploy an Architect project from GitHub Actions with keys gathered from aws-actions/configure-aws-credentials

Deploy an Architect project from GitHub Actions with keys gathered from a specific AWS IAM Role federated by an IAM OIDCProvider. CloudFormation to cr

Taylor Beseda 4 Apr 6, 2022
Magically create forms + actions in Remix!

Welcome to Remix Forms! This repository contains the Remix Forms source code. We're just getting started and the APIs are unstable, so we appreciate y

Seasoned 321 Dec 29, 2022
Collection of SEO utilities like sitemap, robots.txt, etc. for a Remix Application

Remix SEO Collection of SEO utilities like sitemap, robots.txt, etc. for a Remix Features Generate Sitemap Generate Robots.txt Installation To use it,

Balavishnu V J 128 Dec 21, 2022
Prisma 2+ generator to emit Zod schemas from your Prisma schema

Prisma Zod Generator Automatically generate Zod schemas from your Prisma Schema, and use them to validate your API endpoints or any other use you have

Omar Dulaimi 212 Dec 27, 2022
Generate a zodios (typescript http client with zod validation) from an OpenAPI spec (json/yaml)

openapi-zod-client Generates a zodios (typescript http client with zod validation) from a (json/yaml) OpenAPI spec (or just use the generated schemas/

Alexandre Stahmer 104 Jan 4, 2023
A crash course on Zod - a schema validation library for TypeScript

Zod Crash Course This Zod crash course will give you everything you ever needed to know about Zod - an amazing library for building type-safe AND runt

Total TypeScript 339 Dec 28, 2022
Wrap zod validation errors in user-friendly readable messages

zod-validation-error Wrap zod validation errors in user-friendly readable messages. Features User-friendly readable messages, configurable via options

Causaly 70 Dec 23, 2022
Zenload - "Load couple loaders and apply transform one-by-one

Zenload Load couple loaders and apply transforms one-by-one. Install npm i zenload -g How to use? With env vairable ZENLOAD: NODE_OPTIONS='"--loader

coderaiser 1 Jan 25, 2022
Node.js ESM loader for chaining multiple custom loaders.

ESMultiloader Node.js ESM loader for chaining multiple custom loaders. Fast and lightweight No configuration required, but configurable if needed Usag

jhmaster2000 2 Sep 12, 2022
Remix enables you to build fantastic user experiences for the web and feel happy with the code that got you there. In this workshop, we'll look at some more advanced use cases when building Remix applications.

?? Advanced Remix Workshop Remix enables you to build fantastic user experiences for the web and feel happy with the code that got you there. In this

Frontend Masters 167 Dec 9, 2022
Remix enables you to build fantastic user experiences for the web and feel happy with the code that got you there. Get a jumpstart on Remix with this workshop.

?? Remix Fundamentals Build Better websites with Remix Remix enables you to build fantastic user experiences for the web and feel happy with the code

Frontend Masters 204 Dec 25, 2022
simple-remix-blog is a blog template built using Remix and TailwindCSS. Create your own blog in just a few minutes!

simple-remix-blog is a blog template built using remix.run and TailwindCSS. It supports markdown and MDX for the blog posts. You can clone it and star

José Miguel Álvarez Vañó 8 Dec 8, 2022
The Remix version of the fakebooks app demonstrated on https://remix.run. Check out the CRA version: https://github.com/kentcdodds/fakebooks-cra

Remix Fakebooks App This is a (very) simple implementation of the fakebooks mock app demonstrated on remix.run. There is no database, but there is an

Kent C. Dodds 61 Dec 22, 2022
Remix Stack for deploying to Vercel with remix-auth, Planetscale, Radix UI, TailwindCSS, formatting, linting etc. Written in Typescript.

Remix Synthwave Stack Learn more about Remix Stacks. npx create-remix --template ilangorajagopal/synthwave-stack What's in the stack Vercel deploymen

Ilango 56 Dec 25, 2022