昨天我們完成了 Firebase Storage 的環境設定:
- 建立專案 & Bucket
- 下載服務金鑰
- 設定
.env
今天就要正式進入實戰篇!
想像一下:現在我們的服務需要讓使用者可以上傳大頭貼。
那我們該怎麼做?
👉 就是用 Node.js + multer 串接 Firebase Storage,把檔案安全地存到雲端,最後產生一個公開可存取的 URL。
步驟 1:安裝必要套件
1 2 3 4 5
| npm install multer firebase-admin
npm install -D @types/multer
|
套件說明:
multer
: 處理 multipart/form-data
的文件上傳中間件
firebase-admin
: Firebase Admin SDK,用於操作 Firebase Storage
步驟 2:設定環境變數 (.env)
在 .env
檔案中加入 Firebase 設定(詳細可參考 Day16 文章)
1 2 3
| FIREBASE_SERVICE_ACCOUNT={"type":"service_account",...,"client_x509_cert_url":"xxx>"} FIREBASE_STORAGE_BUCKET=your-project.appspot.com
|
重要提醒:
FIREBASE_SERVICE_ACCOUNT
必須是完整的 JSON 字串(單行)
- 記得把
.env
加入 .gitignore
,避免洩漏金鑰
步驟 3:新增 User Entity 欄位
在 User.ts
增加 profileUrl
,用來存放大頭貼位址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Entity() export class User { @PrimaryGeneratedColumn("uuid") id!: string;
@Column({ type: "varchar", length: 50, nullable: false }) name!: string;
@Column({ type: "varchar", length: 320, unique: true, nullable: false }) email!: string;
...
@Column({ name: "profile_url", length: 2048, nullable: true }) profileUrl?: string;
... }
|
步驟 4:建立 Firebase 工具檔 (utils/firebaseUtils.ts)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import admin from "firebase-admin"; import dotenv from "dotenv";
dotenv.config();
const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT!);
admin.initializeApp({ credential: admin.credential.cert(serviceAccount), storageBucket: process.env.FIREBASE_STORAGE_BUCKET, });
const bucket = admin.storage().bucket();
export { admin, bucket };
|
步驟 5:建立圖片上傳中間件 (middleware/imageUpload.ts)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import multer from "multer"; import path from "path"; import { Request } from "express"; import { Express } from "express";
const storage = multer.memoryStorage();
const imageFileFilter = ( req: Request, file: Express.Multer.File, cb: multer.FileFilterCallback ) => { const ext = path.extname(file.originalname).toLowerCase(); if (![".jpg", ".png", ".jpeg"].includes(ext)) { return cb(new Error("只接受 JPG/PNG 格式的圖片檔案")); } cb(null, true); };
export const imageUpload = multer({ storage, limits: { fileSize: 2 * 1024 * 1024 }, fileFilter: imageFileFilter, });
|
設計重點:
memoryStorage()
: 檔案存在記憶體中(req.file.buffer
),適合直接上傳到雲端
fileFilter
: 限制只接受圖片格式
limits
: 限制檔案大小為 2 MB
步驟 6:建立上傳 Controller (controllers/uploadController.ts)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| import { Response, NextFunction } from "express"; import path from "path"; import { bucket } from "../utils/firebaseUtils"; import { AuthRequest } from "../middleware/isAuth"; import { AppDataSource } from "../config/db"; import { User } from "../entities/User";
export async function uploadAvatar( req: AuthRequest, res: Response, next: NextFunction ) { try { if (!req.file) { res.status(400).json({ status: "failed", message: "請選擇要上傳的圖片檔案", }); return; }
if (!req.user) { res.status(401).json({ status: "failed", message: "請先登入", }); return; }
const timestamp = Date.now(); const ext = path.extname(req.file.originalname).toLowerCase(); const remotePath = `images/avatars/user-${req.user.id}-${timestamp}${ext}`;
const file = bucket.file(remotePath);
const stream = file.createWriteStream({ metadata: { contentType: req.file.mimetype, }, });
stream.on("error", (err) => next(err));
stream.on("finish", async () => { try { await file.makePublic();
const publicUrl = `https://storage.googleapis.com/${bucket.name}/${remotePath}`;
await AppDataSource.getRepository(User).update( { id: req.user?.id }, { profileUrl: publicUrl } );
res.status(200).json({ status: "success", message: "大頭照上傳成功", data: { avatarUrl: publicUrl }, }); } catch (err) { next(err); } });
stream.end(req.file.buffer); } catch (err) { next(err); } }
|
步驟 7:設定路由 (routes/uploadRoutes.ts)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { Router } from "express"; import { uploadAvatar } from "../controllers/uploadController"; import { imageUpload } from "../middleware/imageUpload"; import { isAuth } from "../middleware/isAuth";
const router = Router();
router.post( "/avatar", isAuth, imageUpload.single("file"), uploadAvatar );
export default router;
|
中間件順序很重要:
- 先驗證使用者身份 (
isAuth
)
- 再處理檔案上傳 (
imageUpload.single("file")
)
- 最後執行業務邏輯 (
uploadAvatar
)
步驟 8:註冊路由到主應用程式 (app.ts)
1 2 3 4 5 6 7 8 9 10 11
| import express from "express"; import uploadRoutes from "./routes/uploadRoutes";
const app = express();
app.use("/api/upload", uploadRoutes);
export default app;
|
步驟 9:上傳範例
使用 Postman 測試:
- 選擇
POST
方法
- URL:
http://localhost:3000/api/upload/avatar
- Headers:
Authorization: Bearer YOUR_JWT_TOKEN
- Body → form-data → Key:
file
(選擇 File 類型) → 選擇圖片
經由 Postman 回傳結果可得知成功上傳!成功後就能拿到一個 Firebase Storage 的公開 URL 🎉

接著,查看一下資料庫 profileUrl
欄位 → 發現已經有正確的 URL 存入

最後,再到我們的 Firebase Storage 查看檔案 :

太棒了!我們用 Node.js + multer 成功串接了 Firebase Storage 服務 🍻
小結
今天我們完成了:
- 使用
multer
處理圖片上傳
- 串接 Firebase Storage
- 把檔案存雲端並取得公開連結
- 更新資料庫,讓使用者能擁有自己的大頭貼
到這裡,我們的服務具備了「圖片上傳」的能力! 🚀
補充資源
commit : use multer and firebase storage to upload file
Github 連結