burger-api
Version:
<div align="center"> <a href="https://burger-api.com"> <img src="https://github.com/user-attachments/assets/0d9b376e-1d89-479a-aa7f-e7ee3c6b2342" alt="BurgerAPI"/> </a> </div>
134 lines (133 loc) • 5.74 kB
JavaScript
/**
* createValidationMiddleware:
* - Precompute a smaller runtime descriptor per method to avoid
* repeatedly touching the full schema object on every request.
* - Uses safeParse (sync) for Zod schemas (no try/catch around safeParse).
* - Parses querystring manually (no URL constructor, no throw).
* - Lazily allocates the errors container only when needed.
* - Avoids unnecessary object allocations for successful paths.
*
* @param schema - The route schema to validate the request against.
* @returns The middleware function that validates the request.
*/
export function createValidationMiddleware(schema) {
// Precompute a minimal descriptor per HTTP method (lowercased)
// This work runs once at middleware creation time (fast startup cost, zero cost on each request for those computations).
const methodMap = {};
// Loop through the schema and create a method map
for (const rawMethod of Object.keys(schema)) {
// Convert the method to lowercase
const key = rawMethod.toLowerCase();
// Get the method schema
const m = schema[rawMethod] || {};
// Create a method map
methodMap[key] = {
paramsSchema: m.params, // URL parameters schema
querySchema: m.query, // Query parameters schema
bodySchema: m.body, // Request body schema
hasParams: !!m.params, // Whether the method has URL parameters
hasQuery: !!m.query, // Whether the method has query parameters
hasBody: !!m.body, // Whether the method has a request body
};
}
/**
* The middleware function that validates the request.
*
* @param req - The request object.
* @returns The next middleware or handler.
*/
return async (req) => {
// If the request has been validated, continue.
if (req.validated) {
return undefined; // Proceed to the next middleware or handler
}
// Determine the HTTP method (in lowercase) to match the schema.
const method = (req.method || 'get').toLowerCase();
// Get the schema for the current method.
const methodSchema = methodMap[method];
// If there's no schema for this method, continue.
if (!methodSchema) {
return undefined; // Proceed to the next middleware or handler
}
// Get the schemas for the current method.
const { paramsSchema, querySchema, bodySchema, hasParams, hasQuery, hasBody, } = methodSchema;
/**
* Object to store validated request data. This will be attached to the
* request object if validation is successful. The object will contain
* validated data for the following fields:
*
* - `params`: Validated URL parameters (if available and schema provided).
* - `query`: Validated query parameters (if available and schema provided).
* - `body`: Validated request body (if JSON and schema provided).
*/
const validated = {};
// Lazy errors — create only if/when an error occurs
let errors = null;
// Validate URL parameters (if available and schema provided).
if (hasParams && req.params && paramsSchema) {
const result = paramsSchema.safeParse(req.params);
if (result.success) {
validated.params = result.data;
}
else {
if (!errors)
errors = {};
errors.params = result.error.issues;
}
}
// Validate query parameters if available and schema provided.
if (hasQuery && querySchema) {
const url = new URL(req.url);
const queryParams = Object.fromEntries(url.searchParams.entries());
const result = querySchema.safeParse(queryParams);
if (result.success) {
validated.query = result.data;
}
else {
if (!errors)
errors = {};
errors.query = result.error.issues;
}
}
// Get the content type header
const contentType = req.headers.get('content-type') ??
req.headers.get('Content-Type') ??
'';
// Validate request body if it's JSON
if (hasBody &&
contentType?.includes('application/json') &&
bodySchema) {
try {
// Attempt to parse the JSON body.
const bodyData = await req.json();
const result = bodySchema.safeParse(bodyData);
if (result.success) {
// Set the validated body.
validated.body = result.data;
// Set the json method to return the validated body.
req.json = async () => result.data;
}
else {
if (!errors)
errors = {};
errors.body = result.error.issues;
}
}
catch (error) {
// Create a message from the error.
const msg = error instanceof Error ? error.message : String(error);
if (!errors)
errors = {};
errors.body = [{ message: msg }];
}
}
if (errors) {
// If validation fails, return a 400 response with error details.
return Response.json({ errors }, { status: 400 });
}
// Attach validated data to the request.
req.validated = validated;
// Continue to the next middleware or handler.
return undefined; // Proceed
};
}