What You’ll Learn
- How to design a user model with Mongoose
- How to validate request data using express-validator
- How to prevent duplicate user registration
- How to create a signup controller with clear error handling
- How to connect route, controller, and validator cleanly
Step 1: Define the User Model (models/User.ts)
A Mongoose model is how we define the structure of our documents in MongoDB.
sh
import * as mongoose from ‘mongoose’; import { model } from ‘mongoose’; const userSchema = new mongoose.Schema({ email: { type: String, required: true }, password: { type: String, required: true }, username: { type: String, required: true }, created_at: { type: Date, required: true, default: new Date() }, updated_at: { type: Date, required: true, default: new Date() }, }); export default model(‘users’, userSchema);
Explanation:
email
,password
, andusername
are required.- We also store timestamps using
created_at
andupdated_at
. - The model name is
'users'
, which MongoDB will convert into theusers
collection.
Step 2: Create Validators (validators/UserValidator.ts)
This file will validate the user input when a signup request is sent.
sh
import { body } from ‘express-validator’; import User from ‘../models/User’; export class UserValidators { static signUp() { return [ body(’email’, ‘Email is Required’) .isEmail() .custom(async (email) => { const existingUser = await User.findOne({ email }); if (existingUser) { throw new Error(‘User already exists’); } return true; }), body(‘password’) .isAlphanumeric() .isLength({ min: 8, max: 20 }) .withMessage(‘Password must be 8–20 characters’), body(‘username’, ‘Username is required’).isString(), ]; } }
Explanation:
.isEmail()
checks for a valid email.custom()
ensures no duplicate user.isAlphanumeric()
and.isLength()
secure the password.isString()
ensures a string-type username
Step 3: Signup Controller (controllers/UserController.ts)
Handles the signup logic after request validation.
sh
import { validationResult } from ‘express-validator’; import User from ‘../models/User’; export class UserController { static async signUp(req, res, next) { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ status_code: 400, message: errors.array()[0].msg, }); } const { email, password, username } = req.body; try { const newUser = new User({ email, password, username }); const savedUser = await newUser.save(); res.status(201).json({ status_code: 201, user: savedUser, }); } catch (err) { next(err); } } }
Explanation:
- Uses
validationResult
to check for errors - If valid, creates a new user instance
- Saves the user to MongoDB and sends a response
Step 4: Setup the Route (routes/UserRouter.ts)
Connects the validator and controller to handle the /signup
route.
sh
import { Router } from ‘express’; import { UserController } from ‘../controllers/UserController’; import { UserValidators } from ‘../validators/UserValidator’; export class UserRouter { public router: Router; constructor() { this.router = Router(); this.postRoutes(); } postRoutes() { this.router.post(‘/signup’, UserValidators.signUp(), UserController.signUp); } } export default new UserRouter().router;
Explanation:
- We apply
UserValidators.signUp()
before calling the controller - Keeps logic clean and modular
Step 5: Testing in Postman
Endpoint: POST http://localhost:5000/api/user/signup
Test Scenarios:
Test | Input | Expected Output |
---|---|---|
Missing email | {} |
Email is Required |
Missing password | { email: "..." } |
Password is Required |
Missing username | { email, password } |
Username is Required |
Duplicate email | Same email twice | User already exists |
Valid input | All fields | 201 Created + user data |
Final Output (Sample Success Response)
sh
{ “status_code”: 201, “user”: { “email”: “[email protected]”, “password”: “password123”, “username”: “testuser”, “created_at”: “2025-06-15T11:08:29.428Z”, “updated_at”: “2025-06-15T11:08:29.428Z”, “_id”: “684eaa881e8f06226a5a97b8”, “__v”: 0 } }