Node.js

CRUD & Validation – Express Router, Controllers & Zod

Build a clean CRUD API with Express Router, a dedicated controller, input validation via Zod or express-validator, and proper async error handling.

Install: Add zod for schema-based validation and express-validator for inline route validation chains.
📄terminal
BASH
npm install zod express-validator
Zod Validation Schema: Define schemas once and reuse them. Use a middleware wrapper to easily validate incoming request bodies.
📄src/validators/userValidator.js
JS
const { z } = require('zod');

const createUserSchema = z.object({
  name:     z.string().min(2).max(100),
  email:    z.string().email(),
  password: z.string().min(6),
  role:     z.enum(['user', 'admin']).default('user'),
});

const updateUserSchema = z.object({
  name:  z.string().min(2).max(100).optional(),
  email: z.string().email().optional(),
  role:  z.enum(['user', 'admin']).optional(),
}).refine(data => Object.keys(data).length > 0, {
  message: "At least one field must be provided for update"
});

// Reusable middleware factory
const validate = (schema) => (req, res, next) => {
  const result = schema.safeParse(req.body);
  
  if (!result.success) {
    const messages = result.error.errors.map((e) => {
      const path = e.path.join('.');
      return path ? `${path}: ${e.message}` : e.message;
    });
    return res.status(422).json({ errors: messages });
  }
  
  req.body = result.data; // use sanitized/defaulted value
  next();
};

module.exports = { createUserSchema, updateUserSchema, validate };
express-validator: Chain validators directly in route definitions. Call validationResult(req) inside the controller to check for errors.
📄src/routes/userRoutes.js (express-validator)
JS
const { body, param, validationResult } = require('express-validator');

// Re-usable helper to check results
const checkValidation = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json({ errors: errors.array() });
  }
  next();
};

const createRules = [
  body('name').trim().notEmpty().withMessage('Name is required'),
  body('email').isEmail().normalizeEmail().withMessage('Valid email required'),
  body('password').isLength({ min: 6 }).withMessage('Min 6 characters'),
  checkValidation,
];

const idRule = [
  param('id').isInt({ min: 1 }).withMessage('ID must be a positive integer'),
  checkValidation,
];

module.exports = { createRules, idRule };
Controller Pattern: Keep controllers thin — delegate DB logic to a service/model layer. Wrap every async handler to forward errors to Express's global error handler.
📄src/controllers/userController.js
JS
const userService = require('../services/userService');

// Wrap async handlers to auto-catch rejections
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

const getAll = asyncHandler(async (req, res) => {
  const { page = 1, limit = 10 } = req.query;
  const result = await userService.paginate(Number(page), Number(limit));
  res.json(result);
});

const getOne = asyncHandler(async (req, res) => {
  const user = await userService.getById(req.params.id);
  if (!user) return res.status(404).json({ message: 'User not found' });
  res.json(user);
});

const create = asyncHandler(async (req, res) => {
  const user = await userService.create(req.body);
  res.status(201).json(user);
});

const update = asyncHandler(async (req, res) => {
  const user = await userService.update(req.params.id, req.body);
  if (!user) return res.status(404).json({ message: 'User not found' });
  res.json(user);
});

const remove = asyncHandler(async (req, res) => {
  const deleted = await userService.remove(req.params.id);
  if (!deleted) return res.status(404).json({ message: 'User not found' });
  res.status(204).send();
});

module.exports = { getAll, getOne, create, update, remove };
Full Router File: Wire routes, middleware, and validation together. The protect middleware guards write operations.
📄src/routes/userRoutes.js
JS
const express  = require('express');
const router   = express.Router();
const ctrl     = require('../controllers/userController');
const { protect }                  = require('../middleware/authMiddleware');
const { createRules, idRule }      = require('./userRules');
const { validate, createUserSchema } = require('../validators/userValidator');

// GET /api/users          – list (paginated)
router.get('/',    protect, ctrl.getAll);

// GET /api/users/:id      – single
router.get('/:id', protect, idRule, ctrl.getOne);

// POST /api/users         – create
router.post('/',
  validate(createUserSchema),   // Zod validation
  ctrl.create
);

// PUT /api/users/:id      – update
router.put('/:id',
  protect,
  idRule,
  ctrl.update
);

// DELETE /api/users/:id   – delete
router.delete('/:id', protect, idRule, ctrl.remove);

module.exports = router;
Global Error Handler: A four-argument Express middleware catches all forwarded errors. Register it after all routes in server.js.
📄src/middleware/errorHandler.js
JS
const errorHandler = (err, req, res, next) => {
  console.error(`[${new Date().toISOString()}] ${err.stack}`);

  // Mongoose validation error
  if (err.name === 'ValidationError') {
    const errors = Object.values(err.errors).map((e) => e.message);
    return res.status(422).json({ errors });
  }

  // Mongoose duplicate key
  if (err.code === 11000) {
    const field = Object.keys(err.keyValue)[0];
    return res.status(409).json({ message: `${field} already exists` });
  }

  // JWT errors
  if (err.name === 'JsonWebTokenError') {
    return res.status(403).json({ message: 'Invalid token' });
  }

  res.status(err.status || 500).json({
    message: err.message || 'Internal Server Error',
  });
};

module.exports = errorHandler;