DevelopmentDesignUI/UXGraphics
About Me
About Me

Building a Modular Authentication System in Node.js

blog thumbnail
nodenextreact
07 November 2024
Creating 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:
Code
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.
Code
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.
Code
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.
Code
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.
Code
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.
Code
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.
Code
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.
Code
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.
Code
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.
Code
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:
Code
1git clone https://github.com/rohitsingh820924/nodejs-backend.git