Overview
Validation in Ship is handled automatically by providing a schema to createEndpoint. When schema is present, the validate middleware auto-applies and merges body + files + query + params into ctx.validatedData.
For additional validation logic (e.g. checking uniqueness), add custom middleware functions to the middlewares array.
This repo uses Zod 4. Use z.email(), z.url(), z.uuid() instead of Zod 3’s z.string().email(). Search existing schemas for patterns.
Examples
Basic validation with createEndpoint
import { z } from 'zod';
import createEndpoint from 'routes/createEndpoint';
import isPublic from 'middlewares/isPublic';
const schema = z.object({
firstName: z.string().min(1, 'Please enter first name.').max(100),
lastName: z.string().min(1, 'Please enter last name.').max(100),
email: z.email('Email format is incorrect.'),
password: z.string().min(6, 'Password must be at least 6 characters'),
});
export default createEndpoint({
method: 'post',
path: '/sign-up',
schema,
middlewares: [isPublic],
async handler(ctx) {
const { firstName, lastName, email, password } = ctx.validatedData;
// ...action code
},
});
Custom validation middleware
To add extra validation logic beyond the Zod schema, add a custom middleware to the middlewares array:
import { z } from 'zod';
import createEndpoint from 'routes/createEndpoint';
import isPublic from 'middlewares/isPublic';
import { userService } from 'resources/user';
const schema = z.object({
email: z.email('Email format is incorrect.'),
password: z.string().min(6, 'Password must be at least 6 characters'),
});
const checkUserExists = async (ctx, next) => {
const { email } = ctx.validatedData;
const exists = await userService.exists({ email });
if (exists) {
ctx.throwClientError({ email: 'User with this email is already registered' });
return;
}
await next();
};
export default createEndpoint({
method: 'post',
path: '/sign-up',
schema,
middlewares: [isPublic, checkUserExists],
async handler(ctx) {
const { email, password } = ctx.validatedData;
// ...action code
},
});
Avoid ctx.assertError() in handlers — it’s a TypeScript assertion function that causes TS2775 without explicit type annotations on ctx. Use ctx.throwError() + return instead.