Chapter 7

Data and Validation

Follow one value from an untrusted request into a validated action payload and then into a typed database write. Request validation and table validation solve different problems and should stay separate in the reader's model.

Validate every trust boundary

Parse route params, query strings, forms, JSON bodies, cookies, and external service payloads before business logic uses them. Prefer parseSafe() in actions when invalid input should become a 400 response; reserve thrown parse errors for boundaries that truly cannot continue.

Schemas, parsing, and issues

Introduce remix/data-schema through objects, primitives, optional/defaulted values, unions and variants, and parse() versus parseSafe(). Cover issue paths, error maps, type inference, and compatibility with other Standard Schema v1 schemas without duplicating the complete API README.

Checks, refinements, transforms, and coercion

Use .pipe(...) with reusable checks, .refine(...) for domain predicates, .transform(...) when validated output changes shape, and remix/data-schema/coerce for string inputs that should become numbers, booleans, dates, or bigints.

FormData and URLSearchParams schemas

Use f.object, f.field, and f.fields for browser form and query-string values, plus f.file and f.files for uploads. Pair form schemas with formData() middleware so parsing happens once and actions read a typed boundary value from context.

Define tables, columns, and relations

Define typed tables with table() and column builders, then model belongsTo, hasOne, hasMany, and through relations. Be explicit that runtime table metadata does not create database constraints: SQL migrations own DDL.

CRUD helpers and query objects

Use find, findOne, findMany, create, update, and delete for common work. Move to standalone query(...) plus db.exec(...), or bound db.query(...), for joins, projections, aggregates, eager relation loading, scoped bulk writes, and reusable query objects. Keep parameterized raw SQL as an escape hatch.

Table validation and lifecycle hooks

Cover beforeWrite, table-level validate, afterWrite, beforeDelete, afterDelete, and afterRead, including their synchronous behavior and partial row shapes. Request schemas validate external input; table hooks enforce persistence invariants across every caller.

Transactions

Use db.transaction() when a set of reads and writes must commit or roll back together. Note that lifecycle hooks do not create implicit transactions and that adapters may expose transaction-specific options.

SQLite, PostgreSQL, and MySQL adapters

Show the shared createDatabase(adapter) shape, then call out runtime-specific SQLite clients, PostgreSQL transaction options, and MySQL's RETURNING and multi-statement limitations where those differences affect app code.

SQL-first migrations

Use timestamped migration directories with required up.sql and optional down.sql. Cover filesystem loading, journaling and checksum drift, up/down, bounded steps or target IDs, dry-run plans, and per-migration transaction modes.

Request-scoped database access

Initialize the database and run migrations before accepting requests, then expose Database through middleware and derive the app context type from that stack. Actions should read the request-scoped database with context.get(Database) or the typed context property installed by the app.