Simple Query Parameter Validator using Custom Policies
Josh Twist
·
January 6, 2023
·
3 min read
How to implement a simple Query Parameter validation approach using Custom Policies
One of the most powerful aspects of Zuplo is the programmable extensibility.
Recently somebody on our Discord channel asked if
we supported query parameter validation as we do
JSON Body validation.
We plan to add this soon as a built-in policy (which will use your OpenAPI
specification). However, I spent 20 minutes building a custom policy to
demonstrate how easy it would be to build a custom policy to support this while
you wait.
This defines a policy for a route (which can be reused on other routes) that
states there are four supported query parameters: foo, bar, wib and ble.
No additional query parameters are allowed.
Note that foo, bar and ble are required, whereas wib is optional.
Each has a different type specified, and the request will be rejected if the
data cannot be parsed as that type from the options int, number, string,
and boolean.
Here are some hits on that URL and associated error responses (status code 400):
Bad RequestRequired query parameter 'foo' missingInvalid value for query parameter 'bar': 'hey' is not a valid numberInvalid value for query parameter 'ble': '23' not a valid boolean value (expect 'true' or false')
Easy peasy - here's the code for that custom policy
ts
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";type SupportedTyped = "int" | "number" | "string" | "boolean";type ParameterValidationRule = { name: string; required?: boolean; type?: SupportedTyped;};type QueryParamValidatorOptions = { params: ParameterValidationRule[]; allowAdditionalParameters?: boolean;};const typeValidators: Record< SupportedTyped, (value: string) => string | undefined> = { int: (value: string) => { const int = parseFloat(value); if (!Number.isInteger(int)) { return `'${value}' is not a valid integer`; } }, number: (value: string) => { const float = parseFloat(value); if (Number.isNaN(float)) { return `'${value}' is not a valid number`; } }, string: (value: string) => { if (value.length === 0) { return `empty string provided`; } }, boolean: (value: string) => { if (!["true", "false"].includes(value)) { return `'${value}' not a valid boolean value (expect 'true' or false')`; } },};export default async function ( request: ZuploRequest, context: ZuploContext, options: QueryParamValidatorOptions, policyName: string,) { const allowAdditionalParameters = options.allowAdditionalParameters ?? false; const q = request.query; const errors: string[] = []; // 1. check no additional parameters if (!allowAdditionalParameters) { const allowedNames = options.params.map((p) => p.name); for (const queryName of Object.keys(q)) { if (!allowedNames.includes(queryName)) { errors.push(`Additional query parameter '${queryName}' not allowed`); } } } // 2. check required and value types for (const param of options.params) { const value = q[param.name]; const required = param.required ?? true; if (!value) { if (!required) { continue; } // required parameter not provided. errors.push(`Required query parameter '${param.name}' missing`); } if (param.type && value) { const validatorResult = typeValidators[param.type](value); if (validatorResult) { errors.push( `Invalid value for query parameter '${param.name}': ${validatorResult}`, ); } } } if (errors.length > 0) { return new Response(`Bad Request\n\n${errors.join("\n")}`, { status: 400 }); } return request;}