---
title: "@playstack/validation"
description: "Zod-backed shared schemas and one portable HTTP validation-error contract."
tags: ["package","foundation","validation","zod","free"]
---

{/* package-access:start */}

> **Free.** MIT licensed. Check preview availability before installing. See [package access](/docs/packages#access-policy).

{/* package-access:end */}

`@playstack/validation` lets browser forms, APIs, and generated OpenAPI descriptions agree on schemas and validation failures.

## Install

```sh
npm install @playstack/core @playstack/validation zod
```

Install the optional Nest peers only when using `@playstack/validation/nest`.

## Compose one schema across boundaries

Keep schemas in an application contract library, parse at the transport edge, and pass only validated values to domain code.

```ts
import { emailSchema, parse, z } from '@playstack/validation'

export const createUserSchema = z.object({
  email: emailSchema,
  displayName: z.string().trim().min(1).max(80),
})

export type CreateUserInput = z.output<typeof createUserSchema>

export function createUser(input: unknown) {
  const command = parse(createUserSchema, input)
  return users.create(command)
}
```

## Stable wire contract

`formatValidationError` and `validationBodyFromError` produce the same response shape for every HTTP framework:

```ts
interface ValidationErrorBody {
  error: { code: string; retryable: boolean; message: string }
  fields: Readonly<Record<string, readonly string[]>>
  form: readonly string[]
}
```

`fields` uses dot-separated schema paths. Issues without a path go in `form`. `malformedJsonErrorBody()` and `refinementFailedErrorBody()` produce the smaller standard error body.

## Configuration reference

The portable package has no global configuration. Parsing functions accept an optional Zod error map:

| API | Input | Result |
| --- | --- | --- |
| `parse(schema, input, options?)` | Any synchronous Zod schema | Validated output or `ValidationFailedError`. |
| `parseAsync(schema, input, options?)` | Schema with async refinements | Validated output or `ValidationFailedError`. |
| `formatValidationError(error, message?)` | A `ZodError` | Portable `ValidationErrorBody`. |
| `validationBodyFromError(error)` | `ValidationFailedError` | Portable `ValidationErrorBody`. |

Built-in schemas cover normalized email, kebab-case slugs, offset ISO timestamps, sort direction, and cursor pagination with a default limit of 25 and maximum of 100.

## NestJS bridge

```ts
import {
  PlaystackValidationPipe,
  cleanupOpenApiDoc,
  createZodDto,
} from '@playstack/validation/nest'

class CreateUserDto extends createZodDto(createUserSchema) {}

@Post()
create(
  @Body(new PlaystackValidationPipe(createUserSchema)) input: CreateUserInput,
) {
  return users.create(input)
}

const document = cleanupOpenApiDoc(SwaggerModule.createDocument(app, config))
```

`PlaystackValidationPipe` accepts the schema and the same optional error-map configuration. Zod issues become HTTP 400 responses; unexpected refinement failures become HTTP 500 responses. DTO and serializer helpers are re-exported from `nestjs-zod` so request, response, and OpenAPI schemas remain aligned.

## React bridge

`applyValidationErrors` works with any form library that exposes a `setError(path, { type, message })` callback:

```ts
import { applyValidationErrors } from '@playstack/validation/react'

const { fieldCount, form } = applyValidationErrors(response, setError)
setFormErrors(form)
```

It returns the number of field messages applied plus the form-level messages; it does not choose a React form library or render error UI.

## Boundary

Validation is not a sanitizer, form library, localization framework, authorization layer, or database-schema generator. The application owns schema placement, localized error maps, and transport-specific parsing of malformed JSON.

{/* package-reference:start */}

## API entry points and requirements

Reference snapshot: `@playstack/validation@0.1.0-beta.1`. Import only the entry point your runtime needs. Paths below are relative to the installed package; use **Go to Definition** in your editor to inspect exact parameters, return types and overloads. Do not import the declaration-file paths directly.

| Public entry point | Declaration file |
| --- | --- |
| `@playstack/validation` | `./dist/index.d.ts` |
| `@playstack/validation/errors` | `./dist/errors.d.ts` |
| `@playstack/validation/nest` | `./dist/nest.d.ts` |
| `@playstack/validation/react` | `./dist/react.d.ts` |
| `@playstack/validation/package.json` | No TypeScript declaration (asset or metadata export). |

Node.js engine requirement: `>=20`. This is not a claim that every entry point works in browsers or Workers.

### Peer dependencies

Keep existing framework versions that satisfy these ranges. Install optional peers only when using the corresponding adapter. The package manager resolves ordinary dependencies separately.

| Peer | Compatible range | When needed |
| --- | --- | --- |
| `@nestjs/common` | `^10.0.0 \|\| ^11.0.0` | Optional; only for the entry points that use it. |
| `@nestjs/swagger` | `^7.4.2 \|\| ^8.0.0 \|\| ^11.0.0` | Optional; only for the entry points that use it. |
| `nestjs-zod` | `^5.5.0` | Optional; only for the entry points that use it. |

For a complete first program, start with [Getting started](/docs/getting-started). For API lookup and partial-example conventions, see [Reading the reference](/docs/packages#reading-the-reference). Provider failures, lifecycle requirements and application responsibilities remain described in the guide above; types alone do not establish production safety.

{/* package-reference:end */}
