Xây dựng ứng dụng quản lý phim với Express, EJS và MongoDB
- 24-07-2026
- Toanngo92
- 0 Comments
Ở bài trước, chúng ta đã hoàn thiện các thao tác MongoDB nền tảng. Bài này bắt đầu ghép các mảnh đó thành một ứng dụng web thực tế: Wonderland Widescreen – hệ thống quản lý và đặt ghế xem phim được xây dựng bằng Node.js, Express, EJS và MongoDB.
Mục tiêu của bài là thiết lập dự án, tổ chức thư mục, tạo giao diện, kết nối MongoDB Atlas và xây dựng các route đầu tiên. Phần cập nhật, xóa phim và hoàn thiện luồng nghiệp vụ sẽ tiếp tục ở bài sau.
Mục lục
1. Kiến trúc và chức năng của ứng dụng
Ứng dụng có hai nhóm người dùng. Khách có thể đăng nhập, xem phim và đặt ghế. Quản trị viên có thêm quyền thêm phim, xóa phim, cập nhật số ghế và xem thông tin đặt chỗ. Giao diện gửi yêu cầu HTTP đến Express; Express xử lý route, đọc hoặc ghi dữ liệu trong MongoDB rồi trả HTML hoặc JSON về trình duyệt.

Express định tuyến bằng các phương thức tương ứng với HTTP như app.get() và app.post(). Khi đường dẫn và phương thức khớp, hàm xử lý route được gọi. Đây là cơ chế cốt lõi để nối các nút, biểu mẫu và thao tác trên giao diện với logic phía máy chủ.
2. Quy trình triển khai

- Kết nối cơ sở dữ liệu: lấy connection string từ MongoDB Atlas và lưu trong biến môi trường.
- Tạo phần giao diện: xây dựng các trang EJS, CSS và JavaScript chạy trên trình duyệt.
- Tạo phần máy chủ: khởi tạo Express, cấu hình middleware, định nghĩa route và gọi MongoDB.
Không nên đặt tên người dùng, mật khẩu hoặc connection string trực tiếp trong mã nguồn. Hãy dùng tệp .env, thêm tệp này vào .gitignore và chỉ chia sẻ một tệp .env.example không chứa bí mật.
3. Khởi tạo dự án Node.js
mkdir movie-application
cd movie-application
npm init -y
npm install express ejs mongodb dotenv
Giáo trình dùng thêm một số gói như body-parser, passport và express-session. Với phần đầu của ứng dụng, Express hiện đã có sẵn express.json() và express.urlencoded(), vì vậy chưa cần cài body-parser. Chỉ thêm session hoặc Passport khi triển khai đăng nhập thực sự.
movie-application/
├── public/
│ ├── css/
│ └── js/
├── views/
│ ├── movie-list.ejs
│ └── add-movie-form.ejs
├── .env
├── .gitignore
├── package.json
└── server.js

4. Cấu hình biến môi trường và MongoDB
Tạo tệp .env. Thay giá trị mẫu bằng connection string thật của cụm Atlas và tên cơ sở dữ liệu của bạn.
MONGODB_URI=mongodb+srv://<username>:<password>@<cluster>/?retryWrites=true&w=majority
DB_NAME=MovieCollection
PORT=3000
Mỗi đối tượng MongoClient có sẵn connection pool, vì vậy nên tạo một client dùng chung thay vì mở kết nối mới trong từng request. Ta kết nối một lần trước khi cho máy chủ lắng nghe cổng.
require('dotenv').config();
const express = require('express');
const { MongoClient } = require('mongodb');
const app = express();
const client = new MongoClient(process.env.MONGODB_URI);
app.set('view engine', 'ejs');
app.set('views', './views');
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(express.static('public'));
let movies;
async function start() {
await client.connect();
const db = client.db(process.env.DB_NAME);
movies = db.collection('MovieCollection');
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server đang chạy tại http://localhost:${port}`);
});
}
start().catch((error) => {
console.error('Không thể khởi động ứng dụng:', error);
process.exit(1);
});
Middleware có quyền truy cập req, res và hàm next. Nếu middleware không tự kết thúc chu kỳ request-response, nó phải gọi next(); nếu không, yêu cầu sẽ bị treo. Trong đoạn trên, ba middleware lần lượt đọc form, đọc JSON và phục vụ CSS/JavaScript/ảnh từ thư mục public.
5. Tạo giao diện danh sách phim bằng EJS
Template engine cho phép viết tệp giao diện có chỗ dành cho dữ liệu động. Khi gọi res.render(), Express đưa dữ liệu vào template EJS rồi trả về HTML hoàn chỉnh cho trình duyệt.

<!-- views/movie-list.ejs -->
<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Wonderland Widescreen</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<main class="container">
<h1>Danh sách phim</h1>
<a href="/movies/new">Thêm phim</a>
<ul>
<% movies.forEach((movie) => { %>
<li>
<strong><%= movie.name %></strong>
— <%= movie.category %>
— còn <%= movie.availableSeats %> ghế
</li>
<% }) %>
</ul>
</main>
</body>
</html>
Dùng thẻ <%= ... %> để xuất giá trị đã được escape. Không nên dùng <%- ... %> với nội dung do người dùng nhập vì có thể tạo lỗ hổng XSS.
6. Route hiển thị phim
app.get('/', (req, res) => {
res.redirect('/movies');
});
app.get('/movies', async (req, res, next) => {
try {
const filter = req.query.category
? { category: req.query.category }
: {};
const movieList = await movies.find(filter).sort({ name: 1 }).toArray();
res.render('movie-list', { movies: movieList });
} catch (error) {
next(error);
}
});
app.get('/api/movies/:name', async (req, res, next) => {
try {
const movie = await movies.findOne({ name: req.params.name });
if (!movie) return res.status(404).json({ message: 'Không tìm thấy phim' });
res.json(movie);
} catch (error) {
next(error);
}
});
Ví dụ trên hỗ trợ lọc bằng URL như /movies?category=Action. API chi tiết dùng route parameter :name. Trong ứng dụng thực tế nên dùng _id hoặc một slug duy nhất thay cho tên, vì hai phim có thể trùng tên.
7. Tạo biểu mẫu thêm phim

<!-- views/add-movie-form.ejs -->
<h1>Thêm phim</h1>
<form method="post" action="/movies">
<label>Tên phim
<input name="name" required maxlength="120">
</label>
<label>Thời lượng (phút)
<input name="duration" type="number" min="1" required>
</label>
<label>Số ghế
<input name="availableSeats" type="number" min="0" required>
</label>
<label>Thể loại
<select name="category" required>
<option value="Action">Hành động</option>
<option value="Comedy">Hài</option>
<option value="Drama">Chính kịch</option>
</select>
</label>
<button type="submit">Thêm phim</button>
</form>
app.get('/movies/new', (req, res) => {
res.render('add-movie-form');
});
app.post('/movies', async (req, res, next) => {
try {
const movie = {
name: String(req.body.name || '').trim(),
duration: Number(req.body.duration),
availableSeats: Number(req.body.availableSeats),
category: req.body.category
};
if (!movie.name || movie.duration < 1 || movie.availableSeats < 0) {
return res.status(400).send('Dữ liệu phim không hợp lệ');
}
await movies.insertOne(movie);
res.redirect('/movies');
} catch (error) {
next(error);
}
});
Thuộc tính HTML như required giúp phản hồi nhanh trên trình duyệt nhưng không thay thế kiểm tra phía máy chủ. Mọi giá trị trong req.body đều phải được coi là dữ liệu không đáng tin cậy, cần chuyển kiểu, kiểm tra phạm vi và giới hạn độ dài trước khi ghi vào MongoDB.
8. Xử lý lỗi tập trung và tắt ứng dụng an toàn
app.use((req, res) => {
res.status(404).send('Không tìm thấy trang');
});
app.use((error, req, res, next) => {
console.error(error);
res.status(500).send('Máy chủ gặp lỗi');
});
async function shutdown() {
await client.close();
process.exit(0);
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
Middleware xử lý lỗi của Express phải có đủ bốn tham số (error, req, res, next). Hãy đặt nó sau tất cả route. Trong môi trường sản xuất, không trả stack trace hoặc thông tin kết nối cho người dùng.
9. Chạy và kiểm tra ứng dụng
node server.js
Mở http://localhost:3000. Kiểm tra lần lượt trang danh sách, bộ lọc thể loại, trang thêm phim, thông báo khi dữ liệu sai và trường hợp truy cập một URL không tồn tại.

10. Bài tập thực hành
- Thêm ba phim mẫu vào collection và hiển thị theo thứ tự tên.
- Tạo ba liên kết lọc Action, Comedy và Drama.
- Bổ sung trường diễn viên và mô tả cho biểu mẫu.
- Hiển thị thông báo thân thiện khi MongoDB tạm thời mất kết nối.
- Tạo ứng dụng quản lý sinh viên gồm tên, tuổi và điểm; có trang danh sách, thêm mới và cập nhật.
Tổng kết
Bạn đã dựng được bộ khung ứng dụng Node.js hoàn chỉnh: Express nhận yêu cầu, EJS tạo HTML động, middleware xử lý dữ liệu đầu vào và MongoDB lưu trữ phim. Bài tiếp theo sẽ hoàn thiện các thao tác cập nhật, xóa, đặt ghế và những trang còn lại của Wonderland Widescreen.
