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
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"]
}
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
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
};
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();
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;
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' });
};
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;
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;
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 });
}
};
Step 7: Start the Server
Run the following command to start the server:
npm run dev
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)