Common REST Recipes
This section is intentionally practical. These are the shapes you are likely to define in a real API.
Recipe: create payload
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:
const result = createUserSchema.create({
email: ' ALEX@EXAMPLE.COM ',
displayName: ' Alex '
})Result:
{
validatedObject: {
email: 'alex@example.com',
displayName: 'Alex',
role: 'member',
marketingOptIn: false
},
errors: {}
}Use this pattern when:
- the client is creating a new resource
- missing
requiredfields should fail - omitted defaults should be materialized
Recipe: patch payload
Use the same schema, but call patch():
const result = createUserSchema.patch({
displayName: ' Updated Name '
})Result:
{
validatedObject: {
displayName: 'Updated Name'
},
errors: {}
}Use this pattern when:
- the client is updating only a subset of fields
- missing
requiredfields 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.
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:
const result = projectDetailSchema.create({
project: {
id: '10',
slug: ' api-redesign '
},
owner: {
id: '7',
email: 'owner@example.com'
},
permissions: ['read', 'write']
})Result:
{
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.
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:
const result = workspaceListSchema.create({
items: [
{ id: '1', slug: 'alpha', ownerUserId: '7' },
{ id: '2', slug: 'beta', ownerUserId: '9' }
],
total: '2'
})Result:
{
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.
const updatePreferencesSchema = createSchema({
userId: { type: 'id', required: true },
preferences: {
type: 'object',
additionalProperties: true
}
})Example:
const result = updatePreferencesSchema.patch({
preferences: {
theme: 'dark',
shortcuts: {
save: 'cmd+s'
},
labs: ['new-sidebar']
}
})Result:
{
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.”
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:
accountSchema.upsert({})Result:
{
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.