Skip to content

Common REST Recipes

This section is intentionally practical. These are the shapes you are likely to define in a real API.

Recipe: create payload

javascript
const createUserSchema = createSchema({
  email: { type: 'string', required: true, notEmpty: true, lowercase: true },
  displayName: { type: 'string', required: true, minLength: 2 },
  role: { type: 'string', defaultTo: 'member' },
  marketingOptIn: { type: 'boolean', defaultTo: false }
})

Use it like this:

javascript
const result = createUserSchema.create({
  email: '  ALEX@EXAMPLE.COM  ',
  displayName: '  Alex  '
})

Result:

javascript
{
  validatedObject: {
    email: 'alex@example.com',
    displayName: 'Alex',
    role: 'member',
    marketingOptIn: false
  },
  errors: {}
}

Use this pattern when:

  • the client is creating a new resource
  • missing required fields should fail
  • omitted defaults should be materialized

Recipe: patch payload

Use the same schema, but call patch():

javascript
const result = createUserSchema.patch({
  displayName: '  Updated Name  '
})

Result:

javascript
{
  validatedObject: {
    displayName: 'Updated Name'
  },
  errors: {}
}

Use this pattern when:

  • the client is updating only a subset of fields
  • missing required fields should not fail just because they were omitted
  • defaults should not be invented during a patch

Recipe: nested detail response

This is a common “show one resource” response shape.

javascript
const userSummarySchema = createSchema({
  id: { type: 'id', required: true },
  email: { type: 'string', required: true }
})

const projectSummarySchema = createSchema({
  id: { type: 'id', required: true },
  slug: { type: 'string', required: true }
})

const projectDetailSchema = createSchema({
  project: {
    type: 'object',
    required: true,
    schema: projectSummarySchema
  },
  owner: {
    type: 'object',
    required: true,
    schema: userSummarySchema
  },
  permissions: {
    type: 'array',
    required: true,
    items: { type: 'string', minLength: 1 }
  }
})

Validate it with create() or replace() depending on your calling style:

javascript
const result = projectDetailSchema.create({
  project: {
    id: '10',
    slug: '  api-redesign  '
  },
  owner: {
    id: '7',
    email: 'owner@example.com'
  },
  permissions: ['read', 'write']
})

Result:

javascript
{
  validatedObject: {
    project: {
      id: 10,
      slug: 'api-redesign'
    },
    owner: {
      id: 7,
      email: 'owner@example.com'
    },
    permissions: ['read', 'write']
  },
  errors: {}
}

Recipe: list response envelope

This library validates objects, so for list endpoints the usual pattern is an envelope object instead of a top-level array.

javascript
const workspaceSummarySchema = createSchema({
  id: { type: 'id', required: true },
  slug: { type: 'string', required: true },
  ownerUserId: { type: 'id', required: true }
})

const workspaceListSchema = createSchema({
  items: {
    type: 'array',
    required: true,
    items: workspaceSummarySchema
  },
  total: { type: 'integer', required: true, min: 0 }
})

Example:

javascript
const result = workspaceListSchema.create({
  items: [
    { id: '1', slug: 'alpha', ownerUserId: '7' },
    { id: '2', slug: 'beta', ownerUserId: '9' }
  ],
  total: '2'
})

Result:

javascript
{
  validatedObject: {
    items: [
      { id: 1, slug: 'alpha', ownerUserId: 7 },
      { id: 2, slug: 'beta', ownerUserId: 9 }
    ],
    total: 2
  },
  errors: {}
}

Recipe: settings or metadata bag

When part of the payload belongs to another layer and should not be field-by-field validated here, use an opaque object bag.

javascript
const updatePreferencesSchema = createSchema({
  userId: { type: 'id', required: true },
  preferences: {
    type: 'object',
    additionalProperties: true
  }
})

Example:

javascript
const result = updatePreferencesSchema.patch({
  preferences: {
    theme: 'dark',
    shortcuts: {
      save: 'cmd+s'
    },
    labs: ['new-sidebar']
  }
})

Result:

javascript
{
  validatedObject: {
    preferences: {
      theme: 'dark',
      shortcuts: {
        save: 'cmd+s'
      },
      labs: ['new-sidebar']
    }
  },
  errors: {}
}

Use this only when you intentionally want:

  • object-ness to be enforced
  • inner keys and values to pass through untouched
  • no nested validation contract owned by this library

Recipe: custom operation for an upsert-like boundary

Sometimes you want “validate the whole shape, apply defaults, but do not require every required field.”

javascript
const accountSchema = createSchema({
  email: { type: 'string', required: true, lowercase: true },
  role: { type: 'string', defaultTo: 'member' }
}, {
  operations: {
    upsert: {
      targetFields: 'schema',
      enforceRequired: false,
      applyDefaults: true,
      outputFields: 'validated'
    }
  }
})

Example:

javascript
accountSchema.upsert({})

Result:

javascript
{
  validatedObject: {
    role: 'member'
  },
  errors: {}
}

This is useful when the persistence layer or surrounding business logic decides whether the resource already exists, and the schema’s job is only to normalize a shared contract.


GPL-3.0-only