Skip to content

Instantly share code, notes, and snippets.

@thom-nic
Last active July 31, 2025 14:56
Show Gist options
  • Save thom-nic/361cc4bb7e7d0d3ee7543b5f774b5286 to your computer and use it in GitHub Desktop.
Save thom-nic/361cc4bb7e7d0d3ee7543b5f774b5286 to your computer and use it in GitHub Desktop.
Helper for ExpressJS v5 to allow `req.query` to be updated in middleware. Examples at the bottom do not use any global monkey-patching so there are no global side-effects.
/* eslint-disable no-invalid-this */
const { request: Request } = require('express');
const parseQuery = Object.getOwnPropertyDescriptor(Request, 'query').get;
function getQuery() {
if (this.__queryModified)
return this.__queryModified;
return parseQuery.call(this);
}
function setQuery(value) {
this.__queryModified = Object.freeze(value);
}
// Change the express.request class definition. This only needs to be called once and could have
// side effects if other code is using the request class and expecting `query` to not be mutable.
function monkeyPatchExpress5RequestQuery() {
Object.defineProperty(Request, 'query', {
configurable: true,
enumerable: true,
get: getQuery,
set: setQuery,
});
};
// Middleware solution provided by 'zecat' from https://stackoverflow.com/a/79604142/213983
// This only patches the instance instead of modifying the class definition. It probably
// comes with a performance penalty since each instance is modified individually.
// This could be combined with the validation middleware, so that requests are only patched *if*
// query validation is performed. If there is a performance impact, it would be limited to
// routes where query validation is necessary, assuming not every route performs query validation.
function writableRequestQueryMiddleware(req, _res, next) {
Object.defineProperty(
req,
'query',
{
...Object.getOwnPropertyDescriptor(req, 'query'),
value: req.query,
writable: true,
});
next();
}
module.exports = {
monkeyPatchExpressRequestQuery,
writableRequestQueryMiddleware,
};
// Trivial example ExpressJS v5 validator using Joi and Boom.
// There are many better ways to do this e.g. as middleware, supporting `body` and `params` etc.
function validateQuery(schema, req, next) {
try {
// this assumes we used either monkeyPatchExpress5RequestQuery() to patch the class, or writableRequestQueryMiddleware() earlier
// in the middleware pipeline to make req.query mutable
req.query = Joi.attempt(req.query, schema);
return true; // valid
}
catch (err) {
// `error` is a Joi ValidationError. You could extract `err.details` and
// put them in your Boom payload.
// See: https://joi.dev/api/?v=17.13.3#validationerror
// See: https://hapi.dev/module/boom/api/?v=10.0.1
next(Boom.conflict('validation error', {data: err.details}));
return false; // invalid
}
}
const Boom = require('@hapi/boom');
const Joi = require('joi');
// This combines the above two examples, and only modifies req.query when it must change due to validation
// req.query remains immutable after changing it here.
// update req.query while keeping the property immutable afterwards
function updateQuery(req, value) {
Object.defineProperty(
req,
'query',
{
...Object.getOwnPropertyDescriptor(req, 'query'),
writable: false,
value,
});
}
// this validator does not require any other middleware or monkey-patching.
// req.query will be updated to the value returned by Joi.attempt() but req.query will
// still be immutable after.
function validateQuery(schema, req, next) {
try {
updateQuery(req, Joi.attempt(req.query, schema));
next(); // valid
}
catch (err) {
// `error` is a Joi ValidationError. You could extract `err.details` and
// put them in your Boom payload.
// See: https://joi.dev/api/?v=17.13.3#validationerror
// See: https://hapi.dev/module/boom/api/?v=10.0.1
next(Boom.conflict('validation error', {data: err.details}));
}
}
/**
* This thunk returns an Express middleware that will validate requests against the given schema.
* See example below.
*/
function validateQueryMiddleware(schema) {
return (req,res,next) => validateQuery(schema,req,next);
}
module.exports = {
validateQueryMiddleware,
validateQuery,
};
const { Router } = require('express')
const { Joi } = require('joi');
const { validateQueryMiddleware } = require('validate_express');
const SEARCH_QUERY_SCHEMA = Joi.object({
q: Joi.string().min(3).max(200).required(),
limit: Joi.number().integer().min(5).max(100).default(30),
page: Joi.number().integer().min(1).default(1),
});
const router = Router();
// GET /search?limit=20&page=3&q=tigers -- passes validation; query values 'limit' and 'page' will be converted to integers
// GET /search?limit=foo -- fails validation: 'q' is required, 'limit' must be an integer
router.get('search',
validateQueryMiddleware(SEARCH_QUERY_SCHEMA),
(req, res, next) => {
const { limit, page, q } = req.query; // these will be validated and limit,page will be integers
// ... route logic
});
module.exports = router;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment