Request Validation
How validation works depends on the stack, and the difference matters:
- Express: validation is explicit. You attach express-validator
chains to a method via
_<verb>Validation. TheswaggerModelschemas drive documentation and type generation, not validation, declaring a schema does not validate the request. - Bun/Elysia: validation is schema-driven. The TypeBox schemas you put on a
route's
body/query/paramsare compiled and enforced by Elysia automatically.
A common misconception: on the Express stack, swaggerModel / Magic Types describe the
API for OpenAPI and generated types. They do not check incoming requests. If you
need to reject bad input, declare a _<verb>Validation chain.
Express
Declaring validation
Add a _<verb>Validation array to the service. Each entry is an express-validator
chain. Efesto runs them before your handler:
import { BaseApiService } from "efesto";
import { body, query, param } from "express-validator";
class Users extends BaseApiService {
constructor() {
super(__filename);
}
_getValidation = [
query("page").optional().isInt({ min: 1 }),
query("limit").optional().isInt({ min: 1, max: 100 }),
];
async _get(req, res) {
return res.json(await listUsers(req.query));
}
_postValidation = [
body("name").isString().isLength({ min: 2, max: 50 }),
body("email").isEmail(),
body("age").optional().isInt({ min: 0, max: 150 }),
];
async _post(req, res) {
return res.status(201).json(await createUser(req.body));
}
}
export default Users;
Validation arrays exist for every verb: _getValidation, _postValidation,
_putValidation, _patchValidation, _deleteValidation, _headValidation,
_optionsValidation, _traceValidation, _connectValidation.
Handling validation errors
Efesto does not impose an error shape. The express-validator results are available
through validationResult(req); format them in your errorMiddleware (the second hook
passed to efesto()), which runs after the validation chains:
import { validationResult } from "express-validator";
const errorMiddleware = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
next();
};
Custom and cross-field validation
Anything express-validator can do works here, because these are express-validator chains. For example, a custom async rule and a cross-field check:
_postValidation = [
body("email").isEmail().custom(async (email) => {
if (await emailExists(email)) throw new Error("Email already in use");
return true;
}),
body("confirmPassword").custom((value, { req }) => {
if (value !== req.body.password) throw new Error("Passwords do not match");
return true;
}),
];
See the express-validator documentation
for the full API (sanitizers, oneOf, schema syntax, and more).
Bun/Elysia
On the Elysia stack, validation is the schema. Attach TypeBox schemas to a route's
body, query, or params; Elysia compiles them, validates each request, and
returns a 422 with a descriptive error when a request doesn't match. The same schema
also documents the endpoint and types the handler.
- Class-based (BaseApiService)
- Native Elysia
import { BaseApiService, type Context, RouteValidation, t } from "efesto/elysia";
export default class extends BaseApiService {
_postValidation: RouteValidation = {
body: t.Object({
name: t.String({ minLength: 2, maxLength: 50 }),
email: t.String({ format: "email" }),
age: t.Optional(t.Number({ minimum: 0, maximum: 150 })),
}),
query: t.Object({ ref: t.Optional(t.String()) }),
};
_post({ body }: Context) {
return createUser(body); // validated against the schema
}
}
import { Elysia, t } from "efesto/elysia";
export default new Elysia().post("/", ({ body }) => createUser(body), {
body: t.Object({
name: t.String({ minLength: 2, maxLength: 50 }),
email: t.String({ format: "email" }),
age: t.Optional(t.Number({ minimum: 0, maximum: 150 })),
}),
});
TypeBox covers the constraints you would reach for, minLength/maxLength,
minimum/maximum, format, pattern, enum (via t.Union/t.Literal), arrays,
and nested objects. See the
Elysia validation guide.
File fields
- Express: validate the non-file fields with
_<verb>Validationas usual; enable the upload with the_<verb>Multerflag (see File Uploads). The file itself (req.file) is checked in your handler. - Elysia (native): describe a file field with
t.File()in the body schema; Elysia validates type and size from the schema. - Elysia (class): enable
_<verb>Multer/_<verb>MultipleMulter; the file arrives inctx.body(see File Uploads).