Building a Modular Authentication System in Node.js

nodenextreact
07 November 2024Creating a scalable backend with Node.js requires organizing your code well, especially as your application grows. For a clean and modular setup, separating models, controllers, routes, and middlewares into their own files is a highly effective approach. In this guide, we'll go through setting up an authentication system in Node.js where we isolate each component and store the schema in a dedicated file.
Project Structure
Organizing the code in a modular way means structuring your files in a way that logically separates each function. Here’s the project structure we’ll be using:
1your_project_name/
2├── config/
3│ └── db.js # Database connection file
4├── controllers/
5│ └── authController.js # Controller functions for authentication
6├── middleware/
7│ └── authMiddleware.js # Middleware for token verification
8├── models/
9│ └── User.js # User model that imports the schema
10├── schemas/
11│ └── userSchema.js # Separate schema file for User
12├── routes/
13│ └── authRoutes.js # Authentication routes
14├── .env # Environment variables
15├── app.js # Main app file
16├── server.js # Server entry point
17└── package.json
1. Database Connection
To connect to MongoDB, we’ll start by setting up a db.js file in a config folder. This file will handle connecting to the MongoDB database, making the main application file cleaner.
1// config/db.js
2const mongoose = require('mongoose');
3
4const connectDB = async () => {
5 try {
6 await mongoose.connect(process.env.DATABASE_URL, {
7 useNewUrlParser: true,
8 useUnifiedTopology: true,
9 });
10 console.log('MongoDB connected');
11 } catch (error) {
12 console.error('Database connection failed:', error.message);
13 process.exit(1);
14 }
15};
16
17module.exports = connectDB;
2. User Schema
In the schemas directory, we create userSchema.js to define the user schema for MongoDB. Separating schemas allows us to organize them in a dedicated folder, making the codebase more readable.
1// schemas/userSchema.js
2const mongoose = require('mongoose');
3const bcrypt = require('bcrypt');
4
5const userSchema = new mongoose.Schema({
6 username: { type: String, required: true, unique: true },
7 password: { type: String, required: true },
8 email: { type: String, required: true, unique: true }
9});
10
11// Password hashing middleware
12userSchema.pre('save', async function (next) {
13 if (!this.isModified('password')) return next();
14 this.password = await bcrypt.hash(this.password, 10);
15 next();
16});
17
18// Password comparison method
19userSchema.methods.comparePassword = async function (password) {
20 return await bcrypt.compare(password, this.password);
21};
22
23module.exports = userSchema;
3. User Model
The User.js file in the models folder imports the userSchema and creates a model with it. This keeps the model and schema modular and maintains separation between model creation and schema definition.
1// models/User.js
2const mongoose = require('mongoose');
3const userSchema = require('../schemas/userSchema');
4
5const User = mongoose.model('User', userSchema);
6module.exports = User;
4. Authentication Controller
In the controllers folder, authController.js handles the logic for registering and logging in users. The controller also includes a profile retrieval function, demonstrating how to build APIs around authentication.
1// controllers/authController.js
2const User = require('../models/User');
3const jwt = require('jsonwebtoken');
4
5exports.register = async (req, res) => {
6 try {
7 const { username, password, email } = req.body;
8 const user = new User({ username, password, email });
9 await user.save();
10 res.status(201).json({ message: 'User registered successfully' });
11 } catch (error) {
12 res.status(400).json({ message: error.message });
13 }
14};
15
16exports.login = async (req, res) => {
17 try {
18 const { username, password } = req.body;
19 const user = await User.findOne({ username });
20 if (!user || !(await user.comparePassword(password))) {
21 return res.status(401).json({ message: 'Invalid credentials' });
22 }
23 const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });
24 res.json({ message: 'Login successful', token });
25 } catch (error) {
26 res.status(500).json({ message: error.message });
27 }
28};
29
30exports.profile = async (req, res) => {
31 try {
32 const user = await User.findById(req.user.id).select('-password');
33 res.json(user);
34 } catch (error) {
35 res.status(500).json({ message: error.message });
36 }
37};
5. Authentication Middleware
The authMiddleware.js file in middleware ensures that routes requiring authentication verify the user’s token before granting access.
1// middleware/authMiddleware.js
2const jwt = require('jsonwebtoken');
3
4exports.authMiddleware = (req, res, next) => {
5 const token = req.headers['authorization'];
6 if (!token) return res.status(401).json({ message: 'Access denied' });
7
8 try {
9 const decoded = jwt.verify(token, process.env.JWT_SECRET);
10 req.user = decoded;
11 next();
12 } catch (error) {
13 res.status(400).json({ message: 'Invalid token' });
14 }
15};
6. Authentication Routes
The authRoutes.js file in routes defines the endpoints for registration, login, and profile fetching.
1// routes/authRoutes.js
2const express = require('express');
3const { register, login, profile } = require('../controllers/authController');
4const { authMiddleware } = require('../middleware/authMiddleware');
5
6const router = express.Router();
7
8router.post('/register', register);
9router.post('/login', login);
10router.get('/profile', authMiddleware, profile);
11
12module.exports = router;
7. Main Application File
In app.js, we set up the Express app, import routes, and connect to the database, keeping this file clean and focused on application setup.
1// app.js
2const express = require('express');
3const dotenv = require('dotenv');
4const connectDB = require('./config/db');
5const authRoutes = require('./routes/authRoutes');
6
7dotenv.config();
8connectDB();
9
10const app = express();
11app.use(express.json());
12app.use('/api/auth', authRoutes);
13
14module.exports = app;
8. Server Entry Point
Lastly, the server.js file starts the server, allowing app.js to focus on app configuration.
1// server.js
2const app = require('./app');
3const PORT = process.env.PORT || 5000;
4
5app.listen(PORT, () => {
6 console.log(`Server is running on port ${PORT}`);
7});
9. Environment Variables
Add your secrets to a .env file.
1PORT=5000
2DATABASE_URL=mongodb://localhost:27017/your_db_name
3JWT_SECRET=your_jwt_secret
Testing Your Authentication API
Now, you can test your API using tools like Postman or curl. Here are the endpoints:
Register: POST /api/auth/register
Body: { "username": "user1", "password": "pass123", "email": "user1@example.com" }
Login: POST /api/auth/login
Body: { "username": "user1", "password": "pass123" }
Response: { "message": "Login successful", "token": "..." }
Profile: GET /api/auth/profile
Headers: Authorization:
Git Repository for the Project
You can find the full code implementation for the project on GitHub. To clone the repository, simply run the following command in your terminal:
1git clone https://github.com/rohitsingh820924/nodejs-backend.git