Query String, gửi Email, Upload File và Local Modules trong Node.js
- 24-07-2026
- Toanngo92
- 0 Comments
Bài này kết hợp bốn nhu cầu rất thường gặp trong ứng dụng Node.js: đọc và tạo query string, gửi email, nhận file upload và tách mã thành local module. Giáo trình giới thiệu node:querystring, Nodemailer, Formidable và CommonJS; phần dưới đây cập nhật ví dụ theo ES Modules, URLSearchParams và các nguyên tắc bảo mật hiện đại.
Mục lục
1. Query string là gì?
Query string là phần sau dấu ? trong URL. Nó gồm các cặp key–value ngăn cách bởi dấu &, thường dùng cho tìm kiếm, lọc, phân trang hoặc trạng thái giao diện.
https://example.com/products?category=book&page=2&sort=price

Query string không phải nơi phù hợp cho mật khẩu, token hoặc dữ liệu nhạy cảm vì URL có thể xuất hiện trong lịch sử, log, analytics và header Referer. Dữ liệu query cũng chỉ là chuỗi; ứng dụng phải validate và chuyển kiểu trước khi sử dụng.
2. URLSearchParams: lựa chọn chuẩn cho code mới
node:querystring vẫn ổn định và có thể nhanh hơn trong một số trường hợp, nhưng không phải API chuẩn của web. Khi hiệu năng không phải nút thắt, URL và URLSearchParams dễ dùng chung giữa Node.js và trình duyệt.
const url = new URL(
'/products?category=book&page=2&tag=node&tag=javascript',
'https://example.com',
);
console.log(url.searchParams.get('category')); // book
console.log(url.searchParams.get('page')); // "2"
console.log(url.searchParams.getAll('tag')); // ["node", "javascript"]
url.searchParams.set('page', '3');
url.searchParams.append('tag', 'backend');
url.searchParams.delete('category');
console.log(url.toString());
Khi cần chuyển query thành object, chú ý rằng Object.fromEntries(params) chỉ giữ giá trị cuối nếu một key lặp lại. Với tham số nhiều giá trị, hãy dùng getAll().
3. Khi nào dùng node:querystring?

import querystring from 'node:querystring';
const parsed = querystring.parse(
'name=Lan&city=Ha+Noi&skill=node&skill=mongodb',
);
console.log(parsed);
const encoded = querystring.stringify({
name: 'Lan',
active: true,
roles: ['developer', 'reviewer'],
});
console.log(encoded);
parse()vàdecode()là alias.stringify()vàencode()là alias.escape()/unescape()xử lý percent-encoding theo quy tắc của module.- Chỉ dùng delimiter tùy chỉnh khi giao thức của bạn thực sự yêu cầu.
Trong HTTP route, không đưa trực tiếp query vào câu lệnh SQL, đường dẫn file hay lệnh hệ thống. Hãy kiểm tra schema, giới hạn độ dài, whitelist giá trị và sử dụng API tham số hóa.
4. Gửi email bằng Nodemailer
Node.js không có built-in module chuyên gửi email. Nodemailer là package bên thứ ba phổ biến, hỗ trợ SMTP và nhiều transport khác.

npm install nodemailer
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT || 587),
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
});
await transporter.verify();
const info = await transporter.sendMail({
from: process.env.MAIL_FROM,
to: '[email protected]',
subject: 'Xác nhận đăng ký',
text: 'Bạn đã đăng ký thành công.',
html: '<p>Bạn đã <strong>đăng ký thành công</strong>.</p>',
});
console.log(info.messageId);
- Không ghi username, password, OAuth token hoặc private key trực tiếp trong mã.
- Ưu tiên OAuth2 hoặc credential dành riêng cho ứng dụng nếu nhà cung cấp hỗ trợ.
- Escape hoặc sanitize nội dung HTML do người dùng cung cấp.
- Gửi email qua hàng đợi trong production để request web không phải chờ SMTP.
- Không log toàn bộ nội dung email hay credential.
5. Upload file với Formidable
File upload dùng multipart/form-data, không nên parse như JSON hoặc URL-encoded body. Formidable là parser dạng streaming, có thể ghi file xuống đĩa và hạn chế sử dụng bộ nhớ.

npm install formidable
import { createServer } from 'node:http';
import { mkdir } from 'node:fs/promises';
import path from 'node:path';
import formidable from 'formidable';
const uploadDir = path.resolve('uploads');
await mkdir(uploadDir, { recursive: true });
const server = createServer(async (request, response) => {
if (request.method !== 'POST' || request.url !== '/upload') {
response.writeHead(404).end('Not found');
return;
}
const form = formidable({
uploadDir,
keepExtensions: true,
multiples: false,
maxFiles: 1,
maxFileSize: 5 * 1024 * 1024,
filter: ({ mimetype }) => mimetype?.startsWith('image/') ?? false,
});
try {
const [, files] = await form.parse(request);
const file = Array.isArray(files.image) ? files.image[0] : files.image;
if (!file) throw new Error('Không có file hợp lệ');
response.writeHead(201, {
'content-type': 'application/json; charset=utf-8',
});
response.end(JSON.stringify({ size: file.size, type: file.mimetype }));
} catch (error) {
response.writeHead(400, {
'content-type': 'application/json; charset=utf-8',
});
response.end(JSON.stringify({ error: error.message }));
}
});
server.listen(3000);
6. Checklist bảo mật file upload
- Giới hạn kích thước, số lượng file và thời gian upload.
- Không tin
originalFilename, phần mở rộng hoặc MIME type từ client. - Tạo tên file ngẫu nhiên; loại bỏ path separator và ký tự nguy hiểm.
- Lưu file ngoài thư mục public nếu không cần truy cập trực tiếp.
- Kiểm tra nội dung thật của file; cân nhắc quét malware.
- Không thực thi file upload và không cho phép ghi đè file hệ thống.
- Xóa file tạm nếu validation hoặc xử lý tiếp theo thất bại.
- Giới hạn quyền filesystem của process Node.js.
Kiểm tra phần mở rộng
.jpgkhông đủ để kết luận file là ảnh. Kẻ tấn công có thể đổi tên bất kỳ dữ liệu nào thành.jpg.
7. Local module với ES Modules
Local module là mã do chính dự án viết và import bằng đường dẫn tương đối. Mỗi tệp có phạm vi riêng, giúp ẩn chi tiết triển khai và chỉ công khai API cần thiết.

// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// app.js
import { add, subtract } from './math.js';
console.log(add(40, 30));
console.log(subtract(40, 10));
Với ESM, khai báo "type": "module" trong package.json và ghi rõ phần mở rộng của local module. Với CommonJS, dùng module.exports để xuất và require('./math.cjs') để nhập.
8. Tổ chức local module dễ bảo trì
| Thư mục | Trách nhiệm |
|---|---|
routes/ | Ánh xạ method và URL tới handler. |
services/ | Nghiệp vụ như gửi email hoặc xử lý upload. |
repositories/ | Truy cập cơ sở dữ liệu. |
utils/ | Hàm thuần nhỏ, dùng chung nhưng có phạm vi rõ ràng. |
config/ | Đọc và validate cấu hình môi trường. |
- Mỗi module nên có một trách nhiệm chính.
- Ưu tiên export nhỏ và rõ ràng; tránh module “tổng hợp mọi thứ”.
- Không tạo dependency vòng giữa các module.
- Truyền dependency vào function/class khi cần kiểm thử dễ hơn.
- Không thực hiện tác vụ I/O bất ngờ ngay khi module được import.
9. Bài tập thực hành
- Phân tích
playerName=Jack&city=Canada&hobby=snowboardingbằngURLSearchParams. - Tạo query string từ object lọc sách và hỗ trợ nhiều giá trị
genre. - Tách cấu hình SMTP và hàm
sendWelcomeEmail()thành local module. - Tạo route upload một ảnh tối đa 2 MB và từ chối loại file không hợp lệ.
- Viết test cho module xử lý query mà không cần khởi động HTTP server.
Tóm lại: dùng URLSearchParams cho query chuẩn web, giữ credential email ngoài mã nguồn, xem file upload là dữ liệu không tin cậy và tách nghiệp vụ thành local module nhỏ. Bốn thói quen này giúp ứng dụng Node.js rõ ràng, an toàn và dễ kiểm thử hơn.
