Tạo một blog đơn giản sử dụng Node.js, TypeScript, MongoDB và ejs
Last updated
Was this helpful?
Last updated
Was this helpful?
Để tạo một blog đơn giản sử dụng Node.js, TypeScript, MongoDB và có giao diện để nhập dữ liệu cho bài viết (post), tag, và chuyên mục (category), bạn có thể làm theo các bước sau:
package.json
{
"name": "myblog",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "nodemon"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"description": "",
"dependencies": {
"body-parser": "^1.20.3",
"ejs": "^3.1.10",
"express": "^4.21.2",
"mongoose": "^8.11.0"
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/mongoose": "^5.11.96",
"@types/node": "^22.13.8",
"nodemon": "^3.1.9",
"ts-node": "^10.9.2",
"typescript": "^5.8.2"
}
}
src\index.ts
import express from 'express';
import mongoose from 'mongoose';
import bodyParser from 'body-parser';
import homeRoutes from './routes/homeRoutes';
import postRoutes from './routes/postRoutes';
import categoryRoutes from './routes/categoryRoutes';
import tagRoutes from './routes/tagRoutes';
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(bodyParser.urlencoded({ extended: true }));
app.set('view engine', 'ejs');
app.set('views', './src/views');
// Routes
app.use('/', homeRoutes);
app.use('/posts', postRoutes);
app.use('/categories', categoryRoutes);
app.use('/tags', tagRoutes);
// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/myblog').then(() => {
console.log('Connected to MongoDB');
}).catch((err) => {
console.error('Failed to connect to MongoDB', err);
});
// Start the server
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
src\views\index.ejs
<!DOCTYPE html>
<html>
<head>
<title>Categories</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
/>
</head>
<body>
<h1>Home</h1>
<p><a target="_blank" href="/posts/create">Posts Create</a></p>
<p><a target="_blank" href="/posts">Posts List</a></p>
<p><a target="_blank" href="/categories/create">Categories Create </a></p>
<p><a target="_blank" href="/categories">Categories List</a></p>
<p><a target="_blank" href="/tags/create">Tags Create </a></p>
<p><a target="_blank" href="/tags">Tags List</a></p>
</body>
</html>
src\views\tag\list.ejs
<!DOCTYPE html>
<html>
<head>
<title>Tags</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
/>
</head>
<body>
<div class="container">
<h1>Tags</h1>
<ul>
<% tags.forEach(tag => { %>
<li><%= tag.name %></li>
<% }) %>
</ul>
</div>
</body>
</html>
src\views\tag\create.ejs
<!DOCTYPE html>
<html>
<head>
<title>Create Tag</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
/>
</head>
<body>
<div class="container">
<h1>Create Tag</h1>
<form action="/tags/create" method="POST">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required />
<button type="submit">Create</button>
</form>
</div>
</body>
</html>
src\views\post\list.ejs
<!DOCTYPE html>
<html>
<head>
<title>Posts</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
/>
</head>
<body>
<div class="container">
<h1>Posts</h1>
<ul>
<% posts.forEach(post => { %>
<li>
<h2><%= post.title %></h2>
<p><%= post.content %></p>
<p>Category: <%= post.category.name %></p>
<p>Tags: <%= post.tags.map(tag => tag.name).join(', ') %></p>
</li>
<% }) %>
</ul>
</div>
</body>
</html>
src\views\post\create.ejs
<!DOCTYPE html>
<html>
<head>
<title>Create Post</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
</head>
<body>
<div class="container">
<h1>Create Post</h1>
<form action="/posts/create" method="POST">
<div class="mb-3">
<label for="title" class="form-label">Title:</label>
<input
type="text"
id="title"
name="title"
required
class="form-control"
/>
</div>
<div class="mb-3">
<label for="content" class="form-label">Content:</label>
<textarea
id="content"
name="content"
required
class="form-control"
></textarea>
</div>
<div class="mb-3">
<label for="category" class="form-label">Category:</label>
<select id="category" name="category" required class="form-select">
<% categories.forEach(category => { %>
<option value="<%= category._id %>"><%= category.name %></option>
<% }) %>
</select>
</div>
<div class="mb-3">
<label for="tags" class="form-label">Tags:</label>
<select id="tags" name="tags" multiple class="form-select">
<% tags.forEach(tag => { %>
<option value="<%= tag._id %>"><%= tag.name %></option>
<% }) %>
</select>
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
</div>
</body>
</html>
src\views\category\list.ejs
<!DOCTYPE html>
<html>
<head>
<title>Categories</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
/>
</head>
<body>
<div class="container">
<h1>Categories</h1>
<ul>
<% categories.forEach(category => { %>
<li><%= category.name %></li>
<% }) %>
</ul>
</div>
</body>
</html>
src\views\category\create.ejs
<!DOCTYPE html>
<html>
<head>
<title>Create Category</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
/>
</head>
<body>
<div class="container">
<h1>Create Category</h1>
<form action="/categories/create" method="POST">
<div class="mb-3">
<label for="name" class="form-label">Name:</label>
<input
type="text"
id="name"
name="name"
required
class="form-control"
/>
<button type="submit" class="btn btn-primary">Create</button>
</div>
</form>
</div>
</body>
</html>
src\routes\tagRoutes.ts
import express from 'express';
import Tag from '../models/Tag';
const router = express.Router();
// Create a new tag
router.get('/create', (req, res) => {
res.render('tag/create');
});
router.post('/create', async (req, res) => {
const { name } = req.body;
const tag = new Tag({ name });
await tag.save();
res.redirect('/tags');
});
// List all tags
router.get('/', async (req, res) => {
const tags = await Tag.find();
res.render('tag/list', { tags });
});
export default router;
src\routes\postRoutes.ts
import express from 'express';
import Post from '../models/Post'; // Import model một lần
import Category from '../models/Category';
import Tag from '../models/Tag';
const router = express.Router();
// Sử dụng model đã import
router.get('/create', async (req, res) => {
const categories = await Category.find();
const tags = await Tag.find();
res.render('post/create', { categories, tags });
});
router.post('/create', async (req, res) => {
const { title, content, category, tags } = req.body;
const post = new Post({ title, content, category, tags });
await post.save();
res.redirect('/posts');
});
router.get('/', async (req, res) => {
const posts = await Post.find().populate('category').populate('tags');
res.render('post/list', { posts });
});
export default router;
src\routes\homeRoutes.ts
import express from 'express';
const router = express.Router();
// Sử dụng model đã import
router.get('/', async (req, res) => {
res.render('index');
});
export default router;
src\routes\categoryRoutes.ts
import express from 'express';
import Category from '../models/Category';
const router = express.Router();
// Create a new category
router.get('/create', (req, res) => {
res.render('category/create');
});
router.post('/create', async (req, res) => {
const { name } = req.body;
const category = new Category({ name });
await category.save();
res.redirect('/categories');
});
// List all categories
router.get('/', async (req, res) => {
const categories = await Category.find();
res.render('category/list', { categories });
});
export default router;
src\models\Tag.ts
import { Schema, model,models, Document } from 'mongoose';
export interface ITag extends Document {
name: string;
}
const tagSchema = new Schema<ITag>({
name: { type: String, required: true, unique: true }
});
export default models.Tag || model<ITag>('Tag', tagSchema);
src\models\Post.ts
import { Schema, model,models, Document } from 'mongoose';
export interface IPost extends Document {
title: string;
content: string;
category: Schema.Types.ObjectId;
tags: Schema.Types.ObjectId[];
}
const postSchema = new Schema<IPost>({
title: { type: String, required: true },
content: { type: String, required: true },
category: { type: Schema.Types.ObjectId, ref: 'Category', required: true },
tags: [{ type: Schema.Types.ObjectId, ref: 'Tag' }]
});
// Kiểm tra xem model đã tồn tại chưa
export default models.Post || model<IPost>('Post', postSchema);
src\models\Category.ts
import mongoose from "mongoose";
const CategorySchema = new mongoose.Schema({
name: { type: String, required: true, unique: true },
});
export default mongoose.models.Category || mongoose.model("Category", CategorySchema);
1.1. Tạo dự án mới
bashCopy
mkdir my-blog
cd my-blog
npm init -y
1.2. Cài đặt các dependencies cần thiết
bashCopy
npm install express mongoose body-parser ejs
npm install --save-dev typescript @types/node @types/express @types/mongoose ts-node nodemon
1.3. Cấu hình TypeScript
Tạo file tsconfig.json
:
jsonCopy
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
1.4. Cấu hình nodemon
để tự động reload server
Tạo file nodemon.json
:
jsonCopy
{
"watch": ["src"],
"ext": "ts",
"ignore": ["src/**/*.test.ts"],
"exec": "ts-node ./src/index.ts"
}
Thêm script vào package.json
:
jsonCopy
"scripts": {
"start": "nodemon"
}
Copy
my-blog/
├── src/
│ ├── models/
│ │ ├── Post.ts
│ │ ├── Category.ts
│ │ └── Tag.ts
│ ├── routes/
│ │ ├── postRoutes.ts
│ │ ├── categoryRoutes.ts
│ │ └── tagRoutes.ts
│ ├── views/
│ │ ├── index.ejs
│ │ ├── post/
│ │ │ ├── create.ejs
│ │ │ └── list.ejs
│ │ ├── category/
│ │ │ ├── create.ejs
│ │ │ └── list.ejs
│ │ └── tag/
│ │ ├── create.ejs
│ │ └── list.ejs
│ ├── index.ts
├── tsconfig.json
├── nodemon.json
├── package.json
└── .env
3.1. Post.ts
typescriptCopy
import { Schema, model, Document } from 'mongoose';
export interface IPost extends Document {
title: string;
content: string;
category: Schema.Types.ObjectId;
tags: Schema.Types.ObjectId[];
}
const postSchema = new Schema<IPost>({
title: { type: String, required: true },
content: { type: String, required: true },
category: { type: Schema.Types.ObjectId, ref: 'Category', required: true },
tags: [{ type: Schema.Types.ObjectId, ref: 'Tag' }]
});
export default model<IPost>('Post', postSchema);
3.2. Category.ts
typescriptCopy
import { Schema, model, Document } from 'mongoose';
export interface ICategory extends Document {
name: string;
}
const categorySchema = new Schema<ICategory>({
name: { type: String, required: true, unique: true }
});
export default model<ICategory>('Category', categorySchema);
3.3. Tag.ts
typescriptCopy
import { Schema, model, Document } from 'mongoose';
export interface ITag extends Document {
name: string;
}
const tagSchema = new Schema<ITag>({
name: { type: String, required: true, unique: true }
});
export default model<ITag>('Tag', tagSchema);
4.1. postRoutes.ts
typescriptCopy
import express from 'express';
import Post from '../models/Post';
import Category from '../models/Category';
import Tag from '../models/Tag';
const router = express.Router();
// Create a new post
router.get('/create', async (req, res) => {
const categories = await Category.find();
const tags = await Tag.find();
res.render('post/create', { categories, tags });
});
router.post('/create', async (req, res) => {
const { title, content, category, tags } = req.body;
const post = new Post({ title, content, category, tags });
await post.save();
res.redirect('/posts');
});
// List all posts
router.get('/', async (req, res) => {
const posts = await Post.find().populate('category').populate('tags');
res.render('post/list', { posts });
});
export default router;
4.2. categoryRoutes.ts
typescriptCopy
import express from 'express';
import Category from '../models/Category';
const router = express.Router();
// Create a new category
router.get('/create', (req, res) => {
res.render('category/create');
});
router.post('/create', async (req, res) => {
const { name } = req.body;
const category = new Category({ name });
await category.save();
res.redirect('/categories');
});
// List all categories
router.get('/', async (req, res) => {
const categories = await Category.find();
res.render('category/list', { categories });
});
export default router;
4.3. tagRoutes.ts
typescriptCopy
import express from 'express';
import Tag from '../models/Tag';
const router = express.Router();
// Create a new tag
router.get('/create', (req, res) => {
res.render('tag/create');
});
router.post('/create', async (req, res) => {
const { name } = req.body;
const tag = new Tag({ name });
await tag.save();
res.redirect('/tags');
});
// List all tags
router.get('/', async (req, res) => {
const tags = await Tag.find();
res.render('tag/list', { tags });
});
export default router;
index.ts
typescriptCopy
import express from 'express';
import mongoose from 'mongoose';
import bodyParser from 'body-parser';
import postRoutes from './routes/postRoutes';
import categoryRoutes from './routes/categoryRoutes';
import tagRoutes from './routes/tagRoutes';
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(bodyParser.urlencoded({ extended: true }));
app.set('view engine', 'ejs');
app.set('views', './src/views');
// Routes
app.use('/posts', postRoutes);
app.use('/categories', categoryRoutes);
app.use('/tags', tagRoutes);
// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/my-blog', {
useNewUrlParser: true,
useUnifiedTopology: true
}).then(() => {
console.log('Connected to MongoDB');
}).catch((err) => {
console.error('Failed to connect to MongoDB', err);
});
// Start the server
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
6.1. views/post/create.ejs
htmlCopy
<!DOCTYPE html>
<html>
<head>
<title>Create Post</title>
</head>
<body>
<h1>Create Post</h1>
<form action="/posts/create" method="POST">
<label for="title">Title:</label>
<input type="text" id="title" name="title" required>
<br>
<label for="content">Content:</label>
<textarea id="content" name="content" required></textarea>
<br>
<label for="category">Category:</label>
<select id="category" name="category" required>
<% categories.forEach(category => { %>
<option value="<%= category._id %>"><%= category.name %></option>
<% }) %>
</select>
<br>
<label for="tags">Tags:</label>
<select id="tags" name="tags" multiple>
<% tags.forEach(tag => { %>
<option value="<%= tag._id %>"><%= tag.name %></option>
<% }) %>
</select>
<br>
<button type="submit">Create</button>
</form>
</body>
</html>
Run HTML
6.2. views/post/list.ejs
htmlCopy
<!DOCTYPE html>
<html>
<head>
<title>Posts</title>
</head>
<body>
<h1>Posts</h1>
<ul>
<% posts.forEach(post => { %>
<li>
<h2><%= post.title %></h2>
<p><%= post.content %></p>
<p>Category: <%= post.category.name %></p>
<p>Tags: <%= post.tags.map(tag => tag.name).join(', ') %></p>
</li>
<% }) %>
</ul>
</body>
</html>
Run HTML
6.3. views/category/create.ejs
htmlCopy
<!DOCTYPE html>
<html>
<head>
<title>Create Category</title>
</head>
<body>
<h1>Create Category</h1>
<form action="/categories/create" method="POST">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
<br>
<button type="submit">Create</button>
</form>
</body>
</html>
Run HTML
6.4. views/category/list.ejs
htmlCopy
<!DOCTYPE html>
<html>
<head>
<title>Categories</title>
</head>
<body>
<h1>Categories</h1>
<ul>
<% categories.forEach(category => { %>
<li><%= category.name %></li>
<% }) %>
</ul>
</body>
</html>
Run HTML
6.5. views/tag/create.ejs
htmlCopy
<!DOCTYPE html>
<html>
<head>
<title>Create Tag</title>
</head>
<body>
<h1>Create Tag</h1>
<form action="/tags/create" method="POST">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
<br>
<button type="submit">Create</button>
</form>
</body>
</html>
Run HTML
6.6. views/tag/list.ejs
htmlCopy
<!DOCTYPE html>
<html>
<head>
<title>Tags</title>
</head>
<body>
<h1>Tags</h1>
<ul>
<% tags.forEach(tag => { %>
<li><%= tag.name %></li>
<% }) %>
</ul>
</body>
</html>
Run HTML
bashCopy
npm start
Bây giờ, bạn có thể truy cập các đường dẫn sau để tạo và xem các bài viết, chuyên mục, và tag:
Tạo bài viết: http://localhost:3000/posts/create
Xem danh sách bài viết: http://localhost:3000/posts
Tạo chuyên mục: http://localhost:3000/categories/create
Xem danh sách chuyên mục: http://localhost:3000/categories
Tạo tag: http://localhost:3000/tags/create
Xem danh sách tag: http://localhost:3000/tags
Trên đây là hướng dẫn cơ bản để tạo một blog đơn giản sử dụng Node.js, TypeScript, MongoDB và EJS. Bạn có thể mở rộng ứng dụng này bằng cách thêm các tính năng như xác thực người dùng, phân trang, tìm kiếm, và nhiều hơn nữa.