[Series] Xây dựng Hệ thống Bất động sản với Node.js & TypeScript - Bài 6: "Hô biến" Tin đăng lung linh với Hình ảnh (Multer & Cloudinary)
Chào anh em! Trong bài này, chúng ta sẽ không lưu ảnh trực tiếp vào Server (vì sẽ làm nặng Server và khó mở rộng). Thay vào đó, chúng ta sẽ sử dụng Cloudinary – một dịch vụ lưu trữ và tối ưu hình ảnh hàng đầu hiện nay. Chúng ta sẽ dùng Multer để bắt dữ liệu từ Client và đẩy thẳng lên mây.
1. Chuẩn bị "Đồ nghề"
Đầu tiên, anh em cần đăng ký một tài khoản miễn phí tại Cloudinary. Sau khi đăng ký, hãy lấy 3 thông số: Cloud Name, API Key, và API Secret.
Cài đặt các thư viện cần thiết:
npm install multer cloudinary multer-storage-cloudinary
npm install @types/multer --save-dev
Cập nhật file .env:
CLOUDINARY_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
2. Cấu hình Cloudinary & Multer Middleware
Chúng ta sẽ tạo một middleware để xử lý việc upload. Middleware này sẽ tự động đẩy ảnh lên Cloudinary và trả về URL cho chúng ta.
File: src/middlewares/upload.middleware.ts
import multer from 'multer';
import { v2 as cloudinary } from 'cloudinary';
import { CloudinaryStorage } from 'multer-storage-cloudinary';
import dotenv from 'dotenv';
dotenv.config();
// Cấu hình Cloudinary
cloudinary.config({
cloud_name: process.env.CLOUDINARY_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
// Thiết lập bộ lưu trữ Cloudinary cho Multer
const storage = new CloudinaryStorage({
cloudinary: cloudinary,
params: async (req, file) => {
return {
folder: 'bds_project/posts', // Thư mục lưu trên Cloudinary
allowed_formats: ['jpg', 'png', 'jpeg', 'webp'],
public_id: `${Date.now()}-${file.originalname.split('.')[0]}`,
};
},
});
export const uploadCloud = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 } // Giới hạn 5MB mỗi ảnh
});
3. Cập nhật Database & Service
Vì trong schema gốc chúng ta chưa có trường lưu danh sách ảnh cho Post, hãy cập nhật lại một chút.
File: prisma/schema.prisma
model posts {
// ... các trường cũ
images String[] // Lưu mảng các URL hình ảnh (PostgreSQL hỗ trợ tốt cái này)
}
(Sau đó chạy: npx prisma generate)
File: src/services/post.service.ts (Cập nhật hàm tạo tin)
export const createPostWithImages = async (userId: number, postData: any, imageUrls: string[]) => {
return await prisma.$transaction(async (tx) => {
// Logic trừ tiền (giữ nguyên từ Bài 5)
// ...
return await tx.posts.create({
data: {
...postData,
price: BigInt(postData.price),
idUser: userId,
images: imageUrls, // Lưu mảng URL vào DB
status: "Còn trống",
}
});
});
};
4. Controller & Routes: Tiếp nhận File
Đây là nơi "ma thuật" xảy ra. Client sẽ gửi nhiều file dưới key tên là images.
File: src/controllers/post.controller.ts
export const handleCreatePost = async (req: AuthRequest, res: Response) => {
try {
// Lấy danh sách URL từ các file đã upload thành công qua middleware
const files = req.files as Express.Multer.File[];
const imageUrls = files ? files.map(file => file.path) : [];
if (imageUrls.length === 0) {
return res.status(400).json({ error: "Vui lòng upload ít nhất 1 hình ảnh thực tế!" });
}
const post = await PostService.createPostWithImages(
req.user!.userId,
req.body,
imageUrls
);
res.status(201).json({
message: "Đăng tin kèm hình ảnh thành công! 📸",
post: JSON.parse(JSON.stringify(post, (k, v) => typeof v === 'bigint' ? v.toString() : v))
});
} catch (err: any) {
res.status(400).json({ error: err.message });
}
};
File: src/routes/post.routes.ts
import { uploadCloud } from '../middlewares/upload.middleware';
// Cho phép upload tối đa 5 ảnh một lần
router.post(
'/',
authenticate,
uploadCloud.array('images', 5),
validate(createPostSchema),
PostController.handleCreatePost
);
5. Hướng dẫn Test Postman (Cực kỳ quan trọng)
Việc test upload file khác hoàn toàn với gửi JSON thông thường, anh em chú ý nhé:
Method: POST
URL: http://localhost:3000/api/posts
Headers: Authorization: Bearer <token> (Phải có token để trừ tiền và định danh).
Body: Chọn kiểu form-data (KHÔNG chọn raw JSON).
Các trường text: Nhập title, price, address... như bình thường.
Trường ảnh:
Key: Nhập images.
Ở cuối ô Key, chọn kiểu từ Text sang File.
Value: Nhấn Select Files và chọn 2-3 tấm ảnh bất động sản đẹp lung linh.
Nhấn Send: Đợi một chút để Cloudinary xử lý. Bạn sẽ nhận được kết quả kèm mảng images chứa các link dạng https://res.cloudinary.com/...
6. Tổng kết
Chúc mừng anh em! Đến đây, hệ thống của chúng ta đã thực sự "thành hình":
Người dùng có thể đăng tin.
Bị trừ phí dịch vụ (Business Logic).
Hình ảnh được tối ưu và lưu trữ an toàn trên Cloud (Infrastructure).
Ở bài tiếp theo (Bài 7), mình sẽ hướng dẫn các bạn cách làm tính năng "Tìm kiếm nâng cao": Lọc tin đăng theo khoảng giá, theo diện tích và theo khu vực (Tỉnh/Thành phố) để người mua dễ dàng tìm thấy căn nhà mơ ước.
Nếu anh em gặp lỗi MulterError: Unexpected field, hãy kiểm tra kỹ xem tên key trong Postman có đúng là images không nhé!
All Rights Reserved