Introduction:
In this tutorial, we’ll build a blog application using Node.js and TypeScript with an event-driven architecture, secured by JSON Web Tokens (JWT). This architecture enables us to create a loosely coupled system where actions can trigger other processes asynchronously. PostgreSQL will serve as our relational database, and we’ll implement JWT authentication for secure access.
Table of Contents
- Setting up the Environment
- Installing Dependencies
- Configuring JWT Authentication
- Setting up the Database
- Creating the Event Emitter
- Defining Models and Controllers
- Building the Event-Driven Workflow
- Testing the Application
1. Setting up the Environment
Begin by creating a project directory and setting up TypeScript and Node.js.
mkdir secure-blog-app
cd secure-blog-app
npm init -y
npx tsc --init
2. Installing Dependencies
Install the following dependencies:
npm install express pg jsonwebtoken bcryptjs sequelize dotenv
npm install typescript @types/node @types/express @types/jsonwebtoken ts-node -D
- Express: For building the web server.
- pg: PostgreSQL client.
- jsonwebtoken: For creating and verifying JWTs.
- bcryptjs: For hashing user passwords.
- sequelize: ORM for easier database interaction.
- dotenv: For environment configuration.
3. Configuring JWT Authentication
Set up environment variables to store sensitive information like JWT secrets.
Create a .env
file:
DATABASE_NAME=blog_db
DATABASE_USER=blog_user
DATABASE_PASSWORD=your_password
JWT_SECRET=your_jwt_secret
In src/config.ts
, load the environment variables:
import dotenv from 'dotenv';
dotenv.config();
export const config = {
database: {
name: process.env.DATABASE_NAME,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
},
jwtSecret: process.env.JWT_SECRET || 'defaultSecret',
};
4. Setting up the Database
Create the PostgreSQL database:
CREATE DATABASE blog_db;
CREATE USER blog_user WITH PASSWORD 'your_password';
GRANT ALL PRIVILEGES ON DATABASE blog_db TO blog_user;
Configure Sequelize to connect to the PostgreSQL database. In src/config/database.ts
:
import { Sequelize } from 'sequelize';
import { config } from './config';
export const sequelize = new Sequelize(config.database.name, config.database.user, config.database.password, {
host: 'localhost',
dialect: 'postgres',
logging: false,
});
5. Creating the Event Emitter
Set up an event emitter to trigger events in the application. In src/EventEmitter.ts
:
import { EventEmitter } from 'events';
class MyEventEmitter extends EventEmitter {}
export const eventEmitter = new MyEventEmitter();
6. Defining Models and Controllers
User and BlogPost Models
Define the models with Sequelize for User
and BlogPost
.
In src/models/User.ts
:
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../config/database';
import bcrypt from 'bcryptjs';
class User extends Model {
public id!: number;
public username!: string;
public password!: string;
// Password hashing
public async setPassword(password: string) {
this.password = await bcrypt.hash(password, 10);
}
}
User.init(
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
password: {
type: DataTypes.STRING,
allowNull: false,
},
},
{
sequelize,
modelName: 'User',
tableName: 'users',
}
);
export default User;
In src/models/BlogPost.ts
:
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../config/database';
class BlogPost extends Model {
public id!: number;
public title!: string;
public content!: string;
public userId!: number;
}
BlogPost.init(
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
title: {
type: DataTypes.STRING,
allowNull: false,
},
content: {
type: DataTypes.TEXT,
allowNull: false,
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
},
},
{
sequelize,
modelName: 'BlogPost',
tableName: 'blog_posts',
}
);
export default BlogPost;
7. Building the Event-Driven Workflow
- JWT Authentication: Create tokens and protect routes.
- Event Listeners: Set up listeners to act on events (e.g., notify on post creation).
Authentication Controller
Create src/controllers/authController.ts
:
import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import User from '../models/User';
import bcrypt from 'bcryptjs';
import { config } from '../config';
export const register = async (req: Request, res: Response) => {
const { username, password } = req.body;
try {
const user = new User({ username });
await user.setPassword(password);
await user.save();
res.status(201).json({ message: 'User registered' });
} catch (error) {
res.status(400).json({ error: error.message });
}
};
export const login = async (req: Request, res: Response) => {
const { username, password } = req.body;
try {
const user = await User.findOne({ where: { username } });
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ id: user.id }, config.jwtSecret, { expiresIn: '1h' });
res.json({ token });
} catch (error) {
res.status(500).json({ error: error.message });
}
};
Blog Controller
In src/controllers/blogController.ts
:
import { Request, Response } from 'express';
import BlogPost from '../models/BlogPost';
import { eventEmitter } from '../EventEmitter';
export const createPost = async (req: Request, res: Response) => {
const { title, content } = req.body;
const userId = req.user.id; // JWT decoded payload
try {
const blogPost = await BlogPost.create({ title, content, userId });
eventEmitter.emit('blogCreated', blogPost);
res.status(201).json(blogPost);
} catch (error) {
res.status(500).json({ error: error.message });
}
};
Protect Routes with JWT Middleware
In src/middleware/authMiddleware.ts
:
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { config } from '../config';
export const authenticateJWT = (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.sendStatus(403);
jwt.verify(token, config.jwtSecret, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user; // Add user to request
next();
});
};
8. Testing the Application
- Start the server using
ts-node
:npx ts-node src/app.ts
- Use Postman to:
- Register a new user.
- Log in to receive a JWT.
- Use the JWT to create a new blog post via the
/blog/create
route.
With this setup, our application emits an event when a new blog post is created. You can extend this by adding more services that respond to events like sending notifications or handling post-publish workflows.
This is a powerful pattern that combines security, scalability, and modularity, suitable for a production-ready application.