TypeScript Blog Project
Introduction
Building a blog is an excellent way to understand how full-stack applications work. In this tutorial, we'll create a blog application using TypeScript, Node.js, and Express. By the end of this tutorial, you'll have built a fully functional blog with user authentication, post creation, editing, and comments.
This project will help you apply TypeScript's type safety features in a real-world application while learning essential web development concepts. Let's begin by understanding what we're going to build.
Project Overview
Our TypeScript blog will have the following features:
- User authentication (signup, login, logout)
- Create, edit, and delete blog posts
- Add and delete comments on posts
- User profiles
- Markdown support for blog content
Here's the architecture we'll be following:
Prerequisites
Before starting, make sure you have:
- Node.js installed (v14 or later recommended)
- Basic knowledge of TypeScript
- Understanding of HTTP and REST APIs
- Familiarity with Express.js (helpful but not required)
Setting Up the Project
Step 1: Project Initialization
First, let's create a new directory and initialize our project:
mkdir typescript-blog
cd typescript-blog
npm init -y
Step 2: Install Required Dependencies
npm install express express-session mongoose bcrypt jsonwebtoken dotenv
npm install --save-dev typescript ts-node @types/express @types/node nodemon @types/express-session @types/bcrypt @types/jsonwebtoken
Step 3: Configure TypeScript
Create a tsconfig.json
file in the root directory:
{
"compilerOptions": {
"target": "ES2018",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}
Step 4: Create the Project Structure
Set up the following directory structure:
typescript-blog/
├── src/
│ ├── config/
│ ├── controllers/
│ ├── interfaces/
│ ├── middlewares/
│ ├── models/
│ ├── routes/
│ ├── services/
│ ├── utils/
│ └── app.ts
├── public/
│ ├── css/
│ ├── js/
│ └── images/
├── views/
├── .env
├── package.json
└── tsconfig.json
Building the Application
Step 5: Set Up Environment Variables
Create a .env
file in the root directory:
PORT=3000
MONGODB_URI=mongodb://localhost:27017/typescript-blog
JWT_SECRET=your_jwt_secret_key
Step 6: Define Interfaces
Let's create our TypeScript interfaces. Create src/interfaces/user.interface.ts
:
export interface IUser {
id?: string;
name: string;
email: string;
password: string;
bio?: string;
createdAt?: Date;
updatedAt?: Date;
}
export interface AuthRequest extends Request {
user?: IUser;
}
Create src/interfaces/post.interface.ts
:
export interface IPost {
id?: string;
title: string;
content: string;
author: string | IUser;
tags?: string[];
comments?: IComment[];
createdAt?: Date;
updatedAt?: Date;
}
Create src/interfaces/comment.interface.ts
:
import { IUser } from './user.interface';
export interface IComment {
id?: string;
content: string;
author: string | IUser;
post: string | IPost;
createdAt?: Date;
updatedAt?: Date;
}
Step 7: Create Database Models
Create src/models/user.model.ts
:
import mongoose, { Schema, Document } from 'mongoose';
import bcrypt from 'bcrypt';
import { IUser } from '../interfaces/user.interface';
interface UserDocument extends IUser, Document {
comparePassword(candidatePassword: string): Promise<boolean>;
}
const userSchema = new Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
bio: { type: String, default: '' },
}, { timestamps: true });
// Hash password before saving
userSchema.pre<UserDocument>('save', async function(next) {
if (!this.isModified('password')) return next();
try {
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error: any) {
next(error);
}
});
// Compare password method
userSchema.methods.comparePassword = async function(candidatePassword: string): Promise<boolean> {
return bcrypt.compare(candidatePassword, this.password);
};
export const User = mongoose.model<UserDocument>('User', userSchema);
Create src/models/post.model.ts
:
import mongoose, { Schema, Document } from 'mongoose';
import { IPost } from '../interfaces/post.interface';
const postSchema = new Schema({
title: { type: String, required: true },
content: { type: String, required: true },
author: { type: Schema.Types.ObjectId, ref: 'User', required: true },
tags: [{ type: String }],
}, { timestamps: true });
export const Post = mongoose.model<IPost & Document>('Post', postSchema);
Create src/models/comment.model.ts
:
import mongoose, { Schema, Document } from 'mongoose';
import { IComment } from '../interfaces/comment.interface';
const commentSchema = new Schema({
content: { type: String, required: true },
author: { type: Schema.Types.ObjectId, ref: 'User', required: true },
post: { type: Schema.Types.ObjectId, ref: 'Post', required: true },
}, { timestamps: true });
export const Comment = mongoose.model<IComment & Document>('Comment', commentSchema);
Step 8: Create Authentication Middleware
Create src/middlewares/auth.middleware.ts
:
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { AuthRequest } from '../interfaces/user.interface';
import { User } from '../models/user.model';
export const auth = async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
// Get token from header
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ message: 'Authentication required' });
}
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { id: string };
const user = await User.findById(decoded.id);
if (!user) {
return res.status(401).json({ message: 'Authentication failed' });
}
// Add user to request object
req.user = {
id: user._id.toString(),
name: user.name,
email: user.email,
password: user.password,
bio: user.bio
};
next();
} catch (error) {
res.status(401).json({ message: 'Authentication failed' });
}
};