Express
Import guard from mongoose-guard/express and put it in a route between express.json() and your handler. If the body passes, your handler runs and the checked data is on req.validated. If it fails, the client gets a 400 with { errors: [...] } and your handler never runs. It works with Express 4 and Express 5.
import express from "express";import { guard } from "mongoose-guard/express";import { User } from "./models/user.js";
const app = express();app.use(express.json());
app.post("/users", guard(User, { allow: ["name", "email", "password"] }), async (req, res) => { const user = await User.create(req.validated); res.status(201).json({ id: user.id });});Middleware order
Section titled “Middleware order”Run things in this sequence:
- Body parsing (
express.json()). The guard readsreq.body, so it must already be parsed. - Authentication. Reject anonymous requests before doing any work on their bodies.
- The guard.
- Your handler.
router.patch("/me", requireAuth, guard(User, { allow: ["name", "profile.bio"], partial: true }), updateMe);If express.json() is missing, req.body is undefined and every request fails with an invalid_body issue: Expected a JSON object.
req.validated vs req.body
Section titled “req.validated vs req.body”req.body | req.validated | |
|---|---|---|
| Set by | express.json() | mongoose-guard, only when validation passes |
| Contains | everything the client sent | only accepted fields |
| Safe to save | no | yes |
In the default reject mode, the two hold the same content, because any extra field fails the request. In strip mode they differ, and only req.validated is trimmed. Make req.validated a habit either way, so switching modes later can’t open a hole.
mongoose-guard never modifies req.body.
Create the guard once
Section titled “Create the guard once”guard() compiles the allow list and checks it against the schema. Call it when you define the route, as in every example here, not inside a handler:
// Good: compiled once at startupconst signupGuard = guard(User, { allow: ["name", "email", "password"] });router.post("/signup", signupGuard, signup);
// Wasteful: recompiles on every requestrouter.post("/signup", (req, res, next) => guard(User, { allow: ["name"] })(req, res, next), signup);A side benefit: a typo in allow throws a GuardConfigError as soon as the route file loads, so your server won’t even start with a broken allow list.
The default error response
Section titled “The default error response”HTTP/1.1 400 Bad RequestContent-Type: application/json
{"errors":[{"code":"forbidden_field","path":"role","message":"Field is not allowed"}]}To change the status, shape or anything else, pass onInvalid. See Custom error responses.
Errors that aren’t validation failures
Section titled “Errors that aren’t validation failures”Sometimes validation itself breaks: a pre("validate") hook throws, or a custom async validator can’t reach the database. Those errors are passed to next(error), so your normal Express error handler deals with them, usually with a 500:
app.use((error, req, res, next) => { console.error(error); res.status(500).json({ error: "Internal error" });});Different fields for different roles
Section titled “Different fields for different roles”A common need: users can edit some fields of their own record, admins can edit more. Create one guard per role and pick between them:
const userEdit = guard(User, { allow: ["name", "profile.bio"], partial: true });const adminEdit = guard(User, { allow: ["name", "profile.bio", "role", "isVerified", "credits"], partial: true,});
router.patch( "/users/:id", requireAuth, (req, res, next) => (req.user.role === "admin" ? adminEdit : userEdit)(req, res, next), updateUser,);mongoose-guard checks what was sent. Whether this user may edit this record (req.params.id) is still your handler’s job.
Routers
Section titled “Routers”Guards are ordinary middleware, so they work on express.Router(), with router.route(), and inside arrays of middleware:
const router = express.Router();
router .route("/products/:id") .put(guard(Product, { allow: ["name", "price", "stock"] }), replaceProduct) .patch(guard(Product, { allow: ["price", "stock"], partial: true }), updateProduct);
app.use("/api", router);Body size limits
Section titled “Body size limits”mongoose-guard validates whatever express.json() hands it. Limit the size there:
app.use(express.json({ limit: "100kb" }));Express 4 and 5
Section titled “Express 4 and 5”The same code works on both. Express 5 handles rejected promises from middleware on its own and Express 4 doesn’t, so the guard catches its own errors and calls next(error) itself. Either way you don’t need a wrapper like express-async-handler around it.