From f55d2d7d3f3cee085f7fef7592495f699896e577 Mon Sep 17 00:00:00 2001 From: Berkin Anik Date: Sun, 27 Aug 2023 16:43:31 +0300 Subject: [PATCH] feat: add `zod` example --- zod/.eslintrc.js | 4 + zod/.gitignore | 6 ++ zod/README.md | 49 +++++++++ zod/app/lib/product.server.ts | 87 +++++++++++++++ zod/app/lib/utils.server.ts | 16 +++ zod/app/root.tsx | 41 +++++++ zod/app/routes/index.tsx | 170 +++++++++++++++++++++++++++++ zod/app/routes/products.tsx | 198 ++++++++++++++++++++++++++++++++++ zod/package.json | 30 ++++++ zod/public/favicon.ico | Bin 0 -> 16958 bytes zod/remix.config.js | 8 ++ zod/remix.env.d.ts | 2 + zod/sandbox.config.json | 7 ++ zod/tsconfig.json | 22 ++++ 14 files changed, 640 insertions(+) create mode 100644 zod/.eslintrc.js create mode 100644 zod/.gitignore create mode 100644 zod/README.md create mode 100644 zod/app/lib/product.server.ts create mode 100644 zod/app/lib/utils.server.ts create mode 100644 zod/app/root.tsx create mode 100644 zod/app/routes/index.tsx create mode 100644 zod/app/routes/products.tsx create mode 100644 zod/package.json create mode 100644 zod/public/favicon.ico create mode 100644 zod/remix.config.js create mode 100644 zod/remix.env.d.ts create mode 100644 zod/sandbox.config.json create mode 100644 zod/tsconfig.json diff --git a/zod/.eslintrc.js b/zod/.eslintrc.js new file mode 100644 index 000000000..2061cd226 --- /dev/null +++ b/zod/.eslintrc.js @@ -0,0 +1,4 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], +}; diff --git a/zod/.gitignore b/zod/.gitignore new file mode 100644 index 000000000..3f7bf98da --- /dev/null +++ b/zod/.gitignore @@ -0,0 +1,6 @@ +node_modules + +/.cache +/build +/public/build +.env diff --git a/zod/README.md b/zod/README.md new file mode 100644 index 000000000..65f3df402 --- /dev/null +++ b/zod/README.md @@ -0,0 +1,49 @@ +# Zod Example + +This example demonstrates how to use [Zod](https://npm.im/zod) for server-side validation and data transformation in a Remix application. It includes a user registration form and a product listing page. + +In the user registration form, Zod is used to validate and transform POST data which is submitted by the form in the action handler. + +In the product listing page, Zod is used to validate and transform GET query parameters which are used for filtering and pagination in the loader. + +Every validation and data transformation is done on the server-side, so the client can use the app without JavaScript enabled. + +Enjoy Remix's progressively enhanced forms 💿 and Zod's type safety 💎! + +## Preview + +Open this example on [CodeSandbox](https://codesandbox.com): + +[![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/remix-run/examples/tree/main/zod) + +## Example + +### `app/root.tsx` + +A simple error boundary component is added to catch the errors and display error messages. + +### `app/routes/index.tsx` + +This file contains the user registration form and its submission handling logic. It leverages Zod for validating and transforming the POST data. + +### `app/routes/products.tsx` + +This file implements the product listing page, including filters and pagination. It leverages Zod for URL query parameter validation and transforming. A cache control header is added to the response to ensure the page is cached also. + +--- + +Following two files are used for mocking and functionality demonstration. They are not directly related to Zod or Remix. + +#### `app/lib/product.server.ts` + +This file defines the schema for the product data and provides a mock product list. It's used to ensure the type safety of the product data. + +#### `app/lib/utils.server.ts` + +This file contains `isDateFormat` utility which is used for date validation for `` and a function for calculating days until the next birthday. + +## Related Links + +- [Remix Documentation](https://remix.run/docs) +- [Zod Documentation](https://github.com/colinhacks/zod/#zod) +- [Zod: Coercion for Primitives](https://github.com/colinhacks/zod/#coercion-for-primitives) diff --git a/zod/app/lib/product.server.ts b/zod/app/lib/product.server.ts new file mode 100644 index 000000000..4b7bc9520 --- /dev/null +++ b/zod/app/lib/product.server.ts @@ -0,0 +1,87 @@ +import * as z from "zod"; + +const ProductSchema = z.object({ + id: z.number().int().positive(), + name: z.string(), + price: z.number(), +}); + +type Product = z.infer; + +export const mockProducts: Product[] = [ + { + id: 1, + name: "Laptop", + price: 900, + }, + { + id: 2, + name: "Smartphone", + price: 700, + }, + { + id: 3, + name: "T-shirt", + price: 20, + }, + { + id: 4, + name: "Jeans", + price: 50, + }, + { + id: 5, + name: "Running Shoes", + price: 90, + }, + { + id: 6, + name: "Bluetooth Speaker", + price: 50, + }, + { + id: 7, + name: "Dress Shirt", + price: 30, + }, + { + id: 8, + name: "Gaming Console", + price: 350, + }, + { + id: 9, + name: "Sneakers", + price: 120, + }, + { + id: 10, + name: "Watch", + price: 200, + }, + { + id: 11, + name: "Hoodie", + price: 40, + }, + { + id: 12, + name: "Guitar", + price: 300, + }, + { + id: 13, + name: "Fitness Tracker", + price: 80, + }, + { + id: 14, + name: "Backpack", + price: 50, + }, + { + id: 15, + name: "Dumbbell Set", + price: 130, + }, +]; diff --git a/zod/app/lib/utils.server.ts b/zod/app/lib/utils.server.ts new file mode 100644 index 000000000..9be23de44 --- /dev/null +++ b/zod/app/lib/utils.server.ts @@ -0,0 +1,16 @@ +export const isDateFormat = (date: string): boolean => { + return /^\d{4}-\d{2}-\d{2}$/.test(date); +}; + +export const calculateDaysUntilNextBirthday = (birthday: Date): number => { + const today = new Date(); + const currentYear = today.getFullYear(); + const birthdayThisYear = new Date( + currentYear, + birthday.getMonth(), + birthday.getDate(), + ); + const diff = birthdayThisYear.getTime() - today.getTime(); + const days = Math.ceil(diff / (1000 * 3600 * 24)); + return days < 0 ? 365 + days : days; +}; diff --git a/zod/app/root.tsx b/zod/app/root.tsx new file mode 100644 index 000000000..34d670368 --- /dev/null +++ b/zod/app/root.tsx @@ -0,0 +1,41 @@ +import type { MetaFunction } from "@remix-run/node"; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; + +export const meta: MetaFunction = () => ({ + charset: "utf-8", + title: "Remix 💿 + Zod 💎", + viewport: "width=device-width,initial-scale=1", +}); + +export default function App() { + return ( + + + + + + + + + + + + + ); +} + +export const ErrorBoundary = ({ error }: { error: Error }) => { + return ( +
+

App Error

+
{error.message}
+
+ ); +}; diff --git a/zod/app/routes/index.tsx b/zod/app/routes/index.tsx new file mode 100644 index 000000000..0152a9944 --- /dev/null +++ b/zod/app/routes/index.tsx @@ -0,0 +1,170 @@ +import { ActionArgs, json } from "@remix-run/node"; +import { Form, Link, useActionData, useNavigation } from "@remix-run/react"; +import * as z from "zod"; + +import { + calculateDaysUntilNextBirthday, + isDateFormat, +} from "~/lib/utils.server"; + +export const action = async ({ request }: ActionArgs) => { + const formData = await request.formData(); + const payload = Object.fromEntries(formData.entries()); + + const currentDate = new Date(); + + const schema = z.object({ + firstName: z + .string() + .min(2, "Must be at least 2 characters") + .max(50, "Must be less than 50 characters"), + email: z.string().email("Must be a valid email"), + birthday: z.coerce + .date() + .min( + new Date( + currentDate.getFullYear() - 26, + currentDate.getMonth(), + currentDate.getDate(), + ), + "Must be at younger than 25", + ) + .max( + new Date( + currentDate.getFullYear() - 18, + currentDate.getMonth(), + currentDate.getDate(), + ), + "Must be at least 18 years old", + ), + }); + + const parseResult = schema.safeParse(payload); + + if (!parseResult.success) { + const fields = { + firstName: typeof payload.firstName === "string" ? payload.firstName : "", + email: typeof payload.email === "string" ? payload.email : "", + birthday: + typeof payload.birthday === "string" && isDateFormat(payload.birthday) + ? payload.birthday + : "", + }; + + return json( + { + fieldErrors: parseResult.error.flatten().fieldErrors, + fields, + message: null, + }, + { + status: 400, + }, + ); + } + + return json({ + fieldErrors: null, + fields: null, + message: `Hello ${parseResult.data.firstName}! We will send an email to ${ + parseResult.data.email + } for your discount code in ${calculateDaysUntilNextBirthday( + parseResult.data.birthday, + )} days.`, + }); +}; + +const errorTextStyle: React.CSSProperties = { + fontWeight: "bold", + color: "red", + marginInline: 0, + marginBlock: "0.25rem", +}; + +export default function RegisterView() { + const actionData = useActionData(); + const navigation = useNavigation(); + const isSubmitting = navigation.state === "submitting"; + + if (actionData?.message) { + return ( +
+

{actionData.message}

+
+ View Products +
+ ); + } + + return ( +
+

Register for a birthday discount!

+
+
+ + + {actionData?.fieldErrors?.firstName + ? actionData.fieldErrors.firstName.map((error, index) => ( +

+ {error} +

+ )) + : null} +
+ +
+ +
+ + + {actionData?.fieldErrors?.email + ? actionData.fieldErrors.email.map((error, index) => ( +

+ {error} +

+ )) + : null} +
+ +
+ +
+ + + {actionData?.fieldErrors?.birthday + ? actionData.fieldErrors.birthday.map((error, index) => ( +

+ {error} +

+ )) + : null} +
+ +
+ + +
+
+ + View Products + +
+ ); +} diff --git a/zod/app/routes/products.tsx b/zod/app/routes/products.tsx new file mode 100644 index 000000000..f32d8b147 --- /dev/null +++ b/zod/app/routes/products.tsx @@ -0,0 +1,198 @@ +import { LoaderArgs, json, redirect } from "@remix-run/node"; +import { + Form, + Link, + useLoaderData, + useNavigation, + useSubmit, +} from "@remix-run/react"; +import * as z from "zod"; + +import { mockProducts } from "~/lib/product.server"; + +export const loader = ({ request }: LoaderArgs) => { + const URLSearchParams = new URL(request.url).searchParams; + + const schema = z.object({ + name: z.string().optional(), + minPrice: z.coerce.number().min(1).optional(), + maxPrice: z.coerce.number().min(1).optional(), + page: z.coerce.number().min(1).optional(), + size: z.coerce.number().min(5).max(10).step(5).optional(), + }); + + // filter out empty string values from query params + // otherwise zod will throw while coercing them to number + const parseResult = schema.safeParse( + Object.fromEntries( + [...URLSearchParams.entries()].filter(([, v]) => v !== ""), + ), + ); + + if (!parseResult.success) { + console.log(parseResult.error); + throw new Error("Invalid query params"); + } + + const { name, minPrice, maxPrice, page, size } = parseResult.data; + + const products = [...mockProducts].sort((a, b) => a.price - b.price); + let filteredProducts = products; + + if (name) { + filteredProducts = filteredProducts.filter((product) => + product.name.toLowerCase().includes(name.toLowerCase()), + ); + } + + if (minPrice) { + filteredProducts = filteredProducts.filter( + (product) => product.price >= minPrice, + ); + } + + if (maxPrice) { + filteredProducts = filteredProducts.filter( + (product) => product.price <= maxPrice, + ); + } + + const pagination = { + page: page || 1, + size: size || 5, + }; + + const totalPageCount = + Math.ceil(filteredProducts.length / pagination.size) || 1; + + if (pagination.page > totalPageCount) { + // reset page to 1 and redirect on invalid page number + URLSearchParams.set("page", "1"); + return redirect(`?${URLSearchParams.toString()}`, { + status: 303, + }); + } + + const paginatedProducts = filteredProducts.slice( + (pagination.page - 1) * pagination.size, + pagination.page * pagination.size, + ); + + return json( + { + products: paginatedProducts, + searchParams: { + name, + minPrice, + maxPrice, + page: pagination.page, + size: pagination.size, + }, + totalPageCount, + }, + { + headers: { + "Cache-Control": "public, max-age=30, s-maxage=60", + }, + }, + ); +}; + +export default function ProductsView() { + const loaderData = useLoaderData(); + const submit = useSubmit(); // used for select onChange + const navigation = useNavigation(); + const isLoading = navigation.state === "loading"; + + return ( +
+

Products

+ +
+ {/* Filters */} + + {" "} + + {" "} + + {" "} + +
+
    + {loaderData.products.length > 0 ? ( + loaderData.products.map((product) => ( +
  • + + {product.name}{" "} + + + ${product.price} + +
  • + )) + ) : ( +

    No products found

    + )} +
+
+ {/* Pagination */} + + Page {loaderData.searchParams.page} of {loaderData.totalPageCount} + {" "} + {" "} + {" "} + + +
+
+ + Home + +
+ ); +} diff --git a/zod/package.json b/zod/package.json new file mode 100644 index 000000000..14c916822 --- /dev/null +++ b/zod/package.json @@ -0,0 +1,30 @@ +{ + "private": true, + "sideEffects": false, + "scripts": { + "build": "remix build", + "dev": "remix dev", + "start": "remix-serve build", + "typecheck": "tsc" + }, + "dependencies": { + "@remix-run/node": "~1.14.2", + "@remix-run/react": "~1.14.2", + "@remix-run/serve": "~1.14.2", + "isbot": "^3.6.5", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "zod": "^3.22.2" + }, + "devDependencies": { + "@remix-run/dev": "~1.14.2", + "@remix-run/eslint-config": "~1.14.2", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.8", + "eslint": "^8.27.0", + "typescript": "^4.8.4" + }, + "engines": { + "node": ">=14" + } +} diff --git a/zod/public/favicon.ico b/zod/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8830cf6821b354114848e6354889b8ecf6d2bc61 GIT binary patch literal 16958 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 EKbET!0{{R3 literal 0 HcmV?d00001 diff --git a/zod/remix.config.js b/zod/remix.config.js new file mode 100644 index 000000000..adf2a0b5d --- /dev/null +++ b/zod/remix.config.js @@ -0,0 +1,8 @@ +/** @type {import('@remix-run/dev').AppConfig} */ +module.exports = { + ignoredRouteFiles: ["**/.*"], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", +}; diff --git a/zod/remix.env.d.ts b/zod/remix.env.d.ts new file mode 100644 index 000000000..dcf8c45e1 --- /dev/null +++ b/zod/remix.env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/zod/sandbox.config.json b/zod/sandbox.config.json new file mode 100644 index 000000000..f92e0250c --- /dev/null +++ b/zod/sandbox.config.json @@ -0,0 +1,7 @@ +{ + "hardReloadOnChange": true, + "template": "remix", + "container": { + "port": 3000 + } +} diff --git a/zod/tsconfig.json b/zod/tsconfig.json new file mode 100644 index 000000000..20f8a386a --- /dev/null +++ b/zod/tsconfig.json @@ -0,0 +1,22 @@ +{ + "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "resolveJsonModule": true, + "target": "ES2019", + "strict": true, + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + + // Remix takes care of building everything in `remix build`. + "noEmit": true + } +}