DEV Community

Syed Ammar
Syed Ammar

Posted on

1

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.

Image of AssemblyAI tool

Challenge Submission: SpeechCraft - AI-Powered Speech Analysis for Better Communication

SpeechCraft is an advanced real-time speech analytics platform that transforms spoken words into actionable insights. Using cutting-edge AI technology from AssemblyAI, it provides instant transcription while analyzing multiple dimensions of speech performance.

Read full post

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Explore a sea of insights with this enlightening post, highly esteemed within the nurturing DEV Community. Coders of all stripes are invited to participate and contribute to our shared knowledge.

Expressing gratitude with a simple "thank you" can make a big impact. Leave your thanks in the comments!

On DEV, exchanging ideas smooths our way and strengthens our community bonds. Found this useful? A quick note of thanks to the author can mean a lot.

Okay