DEV Community

Syed Ammar
Syed Ammar

Posted on

Implementing an Express-based REST API in TypeScript with MongoDB, JWT-based Authentication, and RBAC

Implementing an Express-based REST API in TypeScript with MongoDB, JWT-based Authentication, and Role-Based Access Control (RBAC)

Creating a robust REST API requires proper architecture, authentication mechanisms, and database integration. This article will guide you through building an Express-based REST API in TypeScript with MongoDB, JWT-based Authentication, and Role-Based Access Control (RBAC).


Prerequisites

  • Node.js and npm installed.
  • Basic understanding of TypeScript, Express, and MongoDB.
  • MongoDB instance (local or cloud, e.g., MongoDB Atlas).

Step 1: Project Setup

1. Initialize the Project

Run the following commands to set up the project:

mkdir express-api-rbac
cd express-api-rbac
npm init -y
npm install typescript ts-node nodemon --save-dev
npm install express mongoose jsonwebtoken bcrypt dotenv cors
npm install @types/express @types/mongoose @types/jsonwebtoken @types/node --save-dev
Enter fullscreen mode Exit fullscreen mode

2. Configure TypeScript

Create a tsconfig.json file:

{
    "compilerOptions": {
        "target": "ES6",
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true,
        "outDir": "dist",
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "skipLibCheck": true,
        "types": ["jest"],
        "moduleResolution": "Node16",
        "module": "Node16"
    },
    "exclude": ["node_modules/"],
    "include": ["src/**/*.ts", "test/**/*.ts"]
}
Enter fullscreen mode Exit fullscreen mode

3. Project Structure

Organize your project as follows:

src/
├── server.ts
├── config/
│   └── config.ts
├── models/
│   └── user.model.ts
├── middleware/
│   ├── auth.middleware.ts
│   └── corsHandler.ts
|   └── loggingHandler.ts
|   └── routeNotFound.ts
├── routes/
│   ├── auth.routes.ts
│   └── user.routes.ts
├── controllers/
│   └── auth.controller.ts
|   └── user.controller.ts
└── utils/
    └── logging.ts
Enter fullscreen mode Exit fullscreen mode

Step 2: Configure MongoDB and other Server configuration

Database Configuration

src/config/config.ts:

import dotenv from 'dotenv';
import mongoose from 'mongoose';
dotenv.config();

export const DEVELOPMENT = process.env.NODE_ENV === 'development';
export const TEST = process.env.NODE_ENV === 'test';

export const MONGO_USER = process.env.MONGO_USER || '';
export const MONGO_PASSWORD = process.env.MONGO_PASSWORD || '';
export const MONGO_URL = process.env.MONGO_URL || '';
export const MONGO_DATABASE = process.env.MONGO_DATABASE || '';
export const MOGO_OPTIONS: mongoose.ConnectOptions = { retryWrites: true, w: 'majority' };

export const SERVER_HOSTNAME = process.env.SERVER_HOSTNAME || 'localhost';
export const SERVER_PORT = process.env.SERVER_PORT ? Number(process.env.SERVER_PORT) : 12345;

export const JWT_SECRET = process.env.JWT_SECRET || 'secret_key';

export const mongo = {
    MONGO_USER,
    MONGO_PASSWORD,
    MONGO_URL,
    MONGO_DATABASE,
    MOGO_OPTIONS,
    MONGO_CONNECTION: `mongodb+srv://${MONGO_USER}:${MONGO_PASSWORD}@${MONGO_DATABASE}.${MONGO_URL}`
};

export const server = {
    SERVER_HOSTNAME,
    SERVER_PORT
};
Enter fullscreen mode Exit fullscreen mode

Connecting to MongoDB

src/server.ts:

import http from 'http';
import express from 'express';
import mongoose from 'mongoose';
import './config/logging';
import { corsHandler } from './middleware/corsHandler';
import { loggingHandler } from './middleware/loggingHandler';
import { routeNotFound } from './middleware/routeNotFound';
import { server, mongo } from './config/config';
import userRouter from './routes/user.routes';
import authRouter from './routes/auth.routes';
export const application = express();
export let httpServer: ReturnType<typeof http.createServer>;

let isConnected = false;

export const Main = async () => {
    logging.log('----------------------------------------');
    logging.log('Initializing API');
    logging.log('----------------------------------------');
    application.use(express.urlencoded({ extended: true }));
    application.use(express.json());

    logging.log('----------------------------------------');
    logging.log('Connect to DB');
    logging.log('----------------------------------------');

    try {
        logging.log('MONGO_CONNECTION: ', mongo.MONGO_CONNECTION);
        if (isConnected) {
            logging.log('----------------------------------------');
            logging.log('Using existing connection');
            logging.log('----------------------------------------');
            return mongoose.connection;
        }

        const connection = await mongoose.connect(mongo.MONGO_CONNECTION, mongo.MOGO_OPTIONS);
        isConnected = true;
        logging.log('----------------------------------------');
        logging.log('Connected to db', connection.version);
        logging.log('----------------------------------------');
    } catch (error) {
        logging.log('----------------------------------------');
        logging.log('Unable to connect to db');
        logging.error(error);
        logging.log('----------------------------------------');
    }

    logging.log('----------------------------------------');
    logging.log('Logging & Configuration');
    logging.log('----------------------------------------');
    application.use(loggingHandler);
    application.use(corsHandler);

    logging.log('----------------------------------------');
    logging.log('Define Controller Routing');
    logging.log('----------------------------------------');
    application.get('/main/healthcheck', (req, res, next) => {
        return res.status(200).json({ hello: 'world!' });
    });

    application.use('/api/users', userRouter);
    application.use('/api/auth', authRouter);

    logging.log('----------------------------------------');
    logging.log('Define Routing Error');
    logging.log('----------------------------------------');
    application.use(routeNotFound);

    logging.log('----------------------------------------');
    logging.log('Starting Server');
    logging.log('----------------------------------------');
    httpServer = http.createServer(application);
    httpServer.listen(server.SERVER_PORT, () => {
        logging.log('----------------------------------------');
        logging.log(`Server started on ${server.SERVER_HOSTNAME}:${server.SERVER_PORT}`);
        logging.log('----------------------------------------');
    });
};

export const Shutdown = () => {
    return new Promise((resolve, reject) => {
        if (httpServer) {
            httpServer.close((err) => {
                if (err) return reject(err);
                resolve(true);
            });
        } else {
            resolve(true);
        }
    });
};

Main();
Enter fullscreen mode Exit fullscreen mode

Step 3: Create the User Model

src/models/user.model.ts:

import mongoose, { Schema } from 'mongoose';

export type UserRole = 'admin' | 'distributor' | 'retailer';

export interface IUser extends Document {
    role: UserRole;
    hash: string;
    name: string;
    email: string;
    password: string;
    contact: string;
    business_name: string;
    address: {
        line1: string;
        city: string;
        state: string;
        zipcode: string;
    };
    geolocation?: { lat: number; long: number };
}

const UserSchema: Schema = new Schema(
    {
        role: { type: String, enum: ['admin', 'distributor', 'retailer'], required: true },
        hash: { type: String },
        name: { type: String, required: true },
        email: { type: String, required: true, unique: true },
        password: { type: String, required: true },
        contact: { type: String, required: true },
        business_name: { type: String, required: true },
        address: {
            line1: String,
            city: String,
            state: String,
            zipcode: String
        },
        geolocation: {
            lat: Number,
            long: Number
        }
    },
    {
        timestamps: true
    }
);

const User = mongoose.model<IUser>('User', UserSchema);

export default User;
Enter fullscreen mode Exit fullscreen mode

Step 4: Implement Middleware

Authentication Middleware

src/middleware/auth.middleware.ts:

import { NextFunction, Request, Response } from 'express';
import jwt from 'jsonwebtoken';

import { IUser, UserRole } from '../models/user.model';
import { JWT_SECRET } from '../config/config';
import mongoose from 'mongoose';

export interface JwtRequest extends Request {
    user?: {
        email: string;
        role: UserRole;
    };
}

export const authentication = (req: JwtRequest, res: Response, next: NextFunction) => {
    const authHeader = req.headers.authorization;
    if (!authHeader) {
        return res.status(401).json('No authorization header found');
    }

    const token = authHeader.split(' ')[1]; // Token structure is 'Bearer <token>'
    try {
        const decoded = jwt.verify(token, JWT_SECRET);
        req.user = decoded as IUser;
        next();
    } catch (error) {
        return res.status(401).json('Invalid token');
    }
};

export function authorizeRoles(allowedRoles: UserRole[]) {
    return (req: JwtRequest, res: Response, next: NextFunction) => {
        const user = req.user;

        if (user && !allowedRoles.includes(user.role)) {
            return res.status(403).json({ message: `Forbidden, you are a ${user.role} and this service is only available for ${allowedRoles}` });
        }

        next();
    };
}

export const generateToken = (_id: mongoose.Types.ObjectId, role: string) => {
    return jwt.sign({ _id, role }, JWT_SECRET, { expiresIn: '1h' });
};
Enter fullscreen mode Exit fullscreen mode

Step 5: Create Routes

Authentication Routes

src/routes/auth.routes.ts:

import express, { Router } from 'express';
import { registerUser, loginUser } from '../controllers/auth.controller';

const authRouter: Router = express.Router();

/**
 * @route   POST /api/auth/register
 * @desc    Create a new user
 * @access  (Public)
 */
authRouter.post('/register', registerUser);

/**
 * @route   POST /api/auth/login
 * @desc    Login a user and get token
 * @access  (Public)
 */
authRouter.post('/login', loginUser);

export default authRouter;
Enter fullscreen mode Exit fullscreen mode

Protected Routes

src/routes/user.routes.ts:

import express, { Router } from 'express';
import { protectedRoute, deleteUser, getUserById, updateUser } from '../controllers/user.controller';
import { authentication, authorizeRoles } from '../middleware/auth.middleware';
const userRouter: Router = express.Router();

/**
 * @route   GET /api/users/protected
 * @desc    A protected route
 * @access  Admin/Distributor/Retailer
 */
userRouter.get('/protected', authentication, protectedRoute);

/**
 * @route   GET /api/users/:id
 * @desc    Get a user by email
 * @access  Admin/Distributor/Retailer (self)
 */
userRouter.get('/:id', authentication, getUserById);

/**
 * @route   DELETE /api/users/:id
 * @desc    Delete a user
 * @access  Admin
 */
userRouter.delete('/:id', authentication, authorizeRoles(['admin']), deleteUser);

/**
 * @route   PUT /users/:id
 * @desc    Update user details
 * @access  Admin/Distributor/Retailer (self)
 */
userRouter.put('/:id', authentication, updateUser);

export default userRouter;

Enter fullscreen mode Exit fullscreen mode

Step 6: Create Controllers

src/controllers/auth.controller.ts:

import { Request, Response } from 'express';
import bcrypt from 'bcrypt';
import User from '../models/user.model';
import { generateToken } from '../middleware/auth.middleware';

//User registration
export const registerUser = async (req: Request, res: Response) => {
    console.log('Register route hit');
    try {
        const { name, email, password, role, contact, business_name, address, geolocation } = req.body;
        const hashedPassword = await bcrypt.hash(password, 10);
        const user = new User({ name, email, password: hashedPassword, role, contact, business_name, address, geolocation });
        await user.save();
        return res.status(201).json({ message: 'User created' });
    } catch (error: any) {
        res.status(400).json({ error: error.message });
    }
};

//User login
export const loginUser = async (req: Request, res: Response) => {
    try {
        const { email, password } = req.body;
        const user = await User.findOne({ email });
        if (!user) {
            return res.status(404).json({ error: 'User does not exist' });
        }
        const isPasswordValid = await bcrypt.compare(password, user.password);
        if (!isPasswordValid) {
            return res.status(401).json({ error: 'Invalid email or password' });
        }
        const token = generateToken(user._id, user.role);
        return res.status(200).json({ token });
    } catch (error: any) {
        res.status(500).json({ error: error.message });
    }
};

Enter fullscreen mode Exit fullscreen mode

Step 7: Start the Server

Run the following command to start the server:

npm run dev
Enter fullscreen mode Exit fullscreen mode

The full source code is available at: https://github.com/syedammar/rest-api-nodejs-typescript

This implementation provides a modular, maintainable API with authentication and role-based access control. You can expand it by adding more routes, models, and business logic as required.

Top comments (0)