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, andusernameare required.- We also store timestamps using
created_atandupdated_at. - The model name is
'users', which MongoDB will convert into theuserscollection.
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
validationResultto 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
}
}