View as Json

File Upload (Cloudinary)

The File Upload component provides a standardized way to handle file uploads in Servercn using Cloudinary as the storage provider.

Docs:



Installation Guide

This component requires additional Servercn components.

npx servercn-cli add file-upload

You will be prompted to select a file upload provider:

? Select file upload provider: » - Use arrow-keys. Return to submit.
>   Cloudinary
    Imagekit

The CLI will then automatically configure the component based on your selected provider.


Prerequisites

You must have a Cloudinary account. Click here if you don't have one.

Define the following environment variables:

PORT="8000"
NODE_ENV="development"
LOG_LEVEL="info"
 
# Cloudinary Configuration
CLOUDINARY_CLOUD_NAME="your-cloud-name"
CLOUDINARY_API_KEY="your-api-key"
CLOUDINARY_API_SECRET="your-api-secret"

Ensure the following configuration is defined:

src/configs/env.ts
import "dotenv-flow/config";
import { z } from "zod";
 
export const envSchema = z.object({
  NODE_ENV: z
    .enum(["development", "test", "production"])
    .default("development"),
 
  PORT: z.string().regex(/^\d+$/, "PORT must be a number").transform(Number),
 
  LOG_LEVEL: z
    .enum(["fatal", "error", "warn", "info", "debug", "trace"])
    .default("info"),
 
  CLOUDINARY_CLOUD_NAME: z.string(),
  CLOUDINARY_API_KEY: z.string(),
  CLOUDINARY_API_SECRET: z.string()
}); 
 
export type Env = z.infer<typeof envSchema>;
 
const result = envSchema.safeParse(process.env);
 
if (!result.success) {
  console.error("❌ Invalid environment configuration");
  console.error(z.prettifyError(result.error));
  process.exit(1);
}
 
export const env: Readonly<Env> = Object.freeze(result.data);
 
export default env;

Basic Implementation

Create a Cloudinary configuration file:

src/configs/cloudinary.ts
import { v2 as cloudinary } from "cloudinary";
import env from "./env";
 
cloudinary.config({
  cloud_name: env.CLOUDINARY_CLOUD_NAME,
  api_key: env.CLOUDINARY_API_KEY,
  api_secret: env.CLOUDINARY_API_SECRET
});
 
export default cloudinary;

Servercn uses multer to handle multipart file uploads.

src/middlewares/upload-file.ts
import multer from "multer";
 
export const ALLOWED_FILE_TYPES = [
  "image/jpeg",
  "image/png",
  "image/webp",
  "video/mp4",
  "video/mpeg",
  "video/quicktime",
  "application/pdf"
];
 
export const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
 
const storage = multer.memoryStorage();
 
const fileFilter: multer.Options["fileFilter"] = (_req, file, cb) => {
  if (!ALLOWED_FILE_TYPES.includes(file.mimetype)) {
    return cb(null, false);
  }
  cb(null, true);
};
 
const upload = multer({
  storage,
  limits: { fileSize: MAX_FILE_SIZE },
  fileFilter
});
 
export default upload;

Services for uploading files to Cloudinary and deleting files from Cloudinary.

src/services/cloudinary.service.ts
import { DeleteApiResponse } from "cloudinary";
import cloudinary from "../configs/cloudinary";
 
export interface UploadOptions {
  folder: string;
  resource_type?: "image" | "video" | "raw" | "auto";
}
 
export interface CloudinaryUploadResult {
  url: string;
  public_id: string;
  size: number;
}
 
export const uploadToCloudinary = (
  buffer: Buffer,
  options: UploadOptions
): Promise<CloudinaryUploadResult> => {
  return new Promise((resolve, reject) => {
    const stream = cloudinary.uploader.upload_stream(
      {
        folder: options.folder || "uploads",
        resource_type: options.resource_type || "auto"
      },
      (error, result) => {
        if (error || !result) {
          return reject(error);
        }
        resolve({
          url: result.secure_url,
          public_id: result.public_id,
          size: result.bytes
        });
      }
    );
 
    stream.end(buffer);
  });
};
 
export const deleteFileFromCloudinary = (
  publicIds: string[]
): Promise<DeleteApiResponse> => {
  return new Promise((resolve, reject) => {
    cloudinary.api.delete_resources(publicIds, (error, result) => {
      if (error || !result) {
        return reject(error);
      }
      resolve(result);
    });
  });
};

Usage Example

src/controllers/upload.controller.ts
import { Request, Response, NextFunction } from "express";
 
import {
  CloudinaryUploadResult,
  deleteFileFromCloudinary,
  uploadToCloudinary
} from "../services/cloudinary.service";
 
import { ApiError } from "../utils/api-error";
import { ApiResponse } from "../utils/api-response";
import { AsyncHandler } from "../utils/async-handler";
 
export const uploadFile = AsyncHandler(
  async (req: Request, res: Response, next: NextFunction) => {
    if (!req.file) {
      return next(ApiError.badRequest("File is required"));
    }
 
    const file = await uploadToCloudinary(req.file.buffer, {
      folder: "uploads/files",
      resource_type: "auto"
    });
 
    return ApiResponse.created(res, "File uploaded successfully", file);
  }
);
 
export const uploadMultipleFile = AsyncHandler(
  async (req: Request, res: Response, next: NextFunction) => {
    const files = req.files as Express.Multer.File[];
 
    if (!files || files.length === 0) {
      return next(ApiError.badRequest("Files are required"));
    }
 
    const results: CloudinaryUploadResult[] = await Promise.all(
      files.map(async file => {
        return await uploadToCloudinary(file.buffer, {
          folder: "uploads/images"
        });
      })
    );
 
    return ApiResponse.created(res, "Files uploaded successfully", results);
  }
);
 
export const deleteFile = AsyncHandler(
  async (req: Request, res: Response, next: NextFunction) => {
    const { public_id } = req.body;
 
    if (!public_id) {
      return next(ApiError.badRequest("File ID is required"));
    }
 
    await deleteFileFromCloudinary([public_id]);
 
    return ApiResponse.Success(res, "File deleted successfully", null, 200);
  }
);

src/routes/upload.routes.ts
import { Router } from "express";
 
import upload from "../middlewares/upload-file";
import {
  deleteFile,
  uploadFile,
  uploadMultipleFile
} from "../controllers/upload.controller";
 
const router = Router();
 
router.post("/file", upload.single("file"), uploadFile);
router.post("/files", upload.array("files", 10), uploadMultipleFile);
router.delete("/", deleteFile);
 
export default router;

src/app.ts
import express, { Application } from "express";
import "dotenv-flow/config";
 
import { errorHandler } from "./middlewares/error-handler";
import { logger } from "./utils/logger";
 
import uploadRoutes from "./routes/upload.routes";
import env from "./configs/env";
 
const app: Application = express();
 
const PORT = env.PORT;
 
// middlewares
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
 
// routes
app.use("/api/uploads", uploadRoutes);
 
// Global error handler
app.use(errorHandler);
 
app.listen(PORT, () => {
  logger.info(`Server is running on http://localhost:${PORT}`);
});
cloudinary
imagekit

File & Folder Structure

Loading files...

Installation

npx servercn-cli add file-upload