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.

Jetbrains image

Is Your CI/CD Server a Prime Target for Attack?

57% of organizations have suffered from a security incident related to DevOps toolchain exposures. It makes sense—CI/CD servers have access to source code, a highly valuable asset. Is yours secure? Check out nine practical tips to protect your CI/CD.

Learn more

Top comments (0)

Jetbrains image

Build Secure, Ship Fast

Discover best practices to secure CI/CD without slowing down your pipeline.

Read more

👋 Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay