Skip to content

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 });
});

Run things in this sequence:

  1. Body parsing (express.json()). The guard reads req.body, so it must already be parsed.
  2. Authentication. Reject anonymous requests before doing any work on their bodies.
  3. The guard.
  4. 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.bodyreq.validated
Set byexpress.json()mongoose-guard, only when validation passes
Containseverything the client sentonly accepted fields
Safe to savenoyes

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.

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 startup
const signupGuard = guard(User, { allow: ["name", "email", "password"] });
router.post("/signup", signupGuard, signup);
// Wasteful: recompiles on every request
router.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.

HTTP/1.1 400 Bad Request
Content-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.

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" });
});

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.

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);

mongoose-guard validates whatever express.json() hands it. Limit the size there:

app.use(express.json({ limit: "100kb" }));

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.