@pfeiferio/express-params
v0.2.0
Published
Express middleware for structured, namespace-aware parameter validation
Maintainers
Readme
@pfeiferio/express-params
Express middleware for structured, namespace-aware parameter validation powered by
@pfeiferio/validator.
Features
- ✅ Namespace-aware parameter validation (
body,query,url,file, or custom) - ✅ Lazy validation — parameters are validated per-route, not globally
- ✅ Built-in
validationOnlymode (dry-run via HTTP header) - ✅ Structured error responses via
errorMiddleware - ✅ Parameter aliasing via
withAlias - ✅ Fully typed with TypeScript
Installation
npm install @pfeiferio/express-paramsBasic Usage
import express from 'express'
import {parameterMiddleware, errorMiddleware} from '@pfeiferio/express-params'
import {createParameter} from '@pfeiferio/validator'
import {checkNumber} from '@pfeiferio/check-primitives'
const paramUserId = createParameter('userId', true).validation((val) => {
return checkNumber(val)
})
const app = express()
app.use(express.json())
app.use(parameterMiddleware({
resolveSearchData: (req) => ({
body: req.body,
query: req.query ?? {},
})
}))
app.post('/users', async (req, res) => {
const {userId} = await req.initParams(container => {
container.addBodyParameter(paramUserId)
})
res.json({userId})
})
app.use(errorMiddleware())
app.listen(3000)Validation Only (Dry-Run)
Clients can trigger validation without executing the handler by sending a header. This is useful for real-time form validation.
app.use(parameterMiddleware({
resolveSearchData: (req) => ({
body: req.body,
query: req.query ?? {},
}),
validationOnly: {
header: 'x-validation-only', // default
value: 'true', // default
}
}))
// Add the middleware to handle the ValidationOnlyException
app.use(validationOnlyMiddleware())The client sends:
POST /users
x-validation-only: trueThe handler is never executed. The response will be:
{
"valid": true,
"data": {
"userId": 1
}
}To disable the mechanism entirely:
validationOnly: falseTo use a dynamic token as the value:
validationOnly: {
value: () => myTokenStore.getCurrent()
}Custom Namespaces
By default body and query are supported. You can add any namespace by extending resolveSearchData and using
addParameter directly:
app.use(parameterMiddleware({
resolveSearchData: (req) => ({
body: req.body,
query: req.query ?? {},
url: req.params ?? {},
})
}))
app.get('/users/:userId', async (req, res) => {
const {userId} = await req.initParams(container => {
container.addParameter(paramUserId, 'url')
})
res.json({userId})
})Parameter Aliasing
By default the key in the result matches the parameter name. Use withAlias to map it to a different key — useful when
the input field name differs from your domain language:
import {withAlias} from '@pfeiferio/express-params'
const paramA = createParameter('a', true).validation((val) => checkNumber(val))
app.post('/users', async (req, res) => {
const {userId} = await req.initParams(container => {
container.addBodyParameter(withAlias(paramA, 'userId'))
})
res.json({userId})
})The parameter is still looked up as a in the request body, but the validated value is returned under userId.
Error Handling
errorMiddleware catches ParameterException and responds with a 400:
app.use(errorMiddleware())For custom error shapes, pass a handler directly:
app.use(errorMiddleware((err, req, res, next) => {
res.status(422).json({errors: err.errorStore.errors})
}))validationOnlyMiddleware also accepts a custom handler:
app.use(validationOnlyMiddleware((err, req, res, next) => {
res.status(200).json({ok: true, data: err.data})
}))For cross-package checks (e.g. when multiple package versions may be installed), use the type guard helpers instead of
instanceof:
import {isParameterError, isValidationOnlyException} from '@pfeiferio/express-params'
app.use((err, req, res, next) => {
if (isParameterError(err)) {
res.status(400).json({errors: err.errorStore.errors})
return
}
next(err)
})Design Goals
- Namespace-aware — validation is not limited to
bodyandquery - Lazy by design — parameters are declared per-route, keeping handlers self-contained
- Composable — parameters are plain objects, reusable across routes
- Escape hatches — every default is overridable
License
MIT
