File Upload (Cloudinary)

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

It abstracts common concerns such as multipart handling, Cloudinary configuration, and secure uploads, while integrating cleanly with the rest of the ServerCN backend utilities.

Official Cloudinary Documentation, Cloudinary Node.js SDK

Features

  • Cloudinary-backed file storage
  • Supports images, videos, and raw files
  • Secure server-side uploads
  • Express-compatible middleware
  • Works seamlessly with ApiError, AsyncHandler, and middleware

Installation Guide

This component requires additional ServerCN components.

๐Ÿ‘‰ You do not need to install any ServerCN components manually. Running this component installer will automatically install all required components. Manual installation is optional and only recommended if you prefer fine-grained components control

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:

MVC: src/configs/env.ts

Feature: src/shared/configs/env.ts

interface Config {
  PORT: number;
  NODE_ENV: string;
  LOG_LEVEL: string;
 
  CLOUDINARY_CLOUD_NAME: string;
  CLOUDINARY_API_KEY: string;
  CLOUDINARY_API_SECRET: string;
}
 
const env: Config = {
  PORT: Number(process.env.PORT) || 3000,
  NODE_ENV: process.env.NODE_ENV || "development",
  LOG_LEVEL: process.env.LOG_LEVEL || "info",
 
  CLOUDINARY_CLOUD_NAME: process.env.CLOUDINARY_CLOUD_NAME!,
  CLOUDINARY_API_KEY: process.env.CLOUDINARY_API_KEY!,
  CLOUDINARY_API_SECRET: process.env.CLOUDINARY_API_SECRET!
};
 
export default env;

Basic Implementation

Create a Cloudinary configuration file:

MVC: src/configs/cloudinary.ts

Feature: src/shared/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.

MVC: src/middlewares/upload-file.ts

Feature: src/shared/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.

MVC: src/services/cloudinary.service.ts or src/utils/cloudinary.ts

Feature: src/modules/upload/upload.service.ts

import { DeleteApiResponse } from "cloudinary";
import cloudinary from "../configs/cloudinary.js";
 
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

MVC: 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);
  }
);

Feature: src/modules/upload/upload.controller.ts

import { NextFunction, Request, Response } from "express";
 
import { ApiResponse } from "../../shared/utils/api-response";
import { AsyncHandler } from "../../shared/utils/async-handler";
import {
  CloudinaryUploadResult,
  deleteFileFromCloudinary,
  uploadToCloudinary
} from "./upload.service";
import { ApiError } from "../../shared/errors/api-error";
 
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);
  }
);

MVC: 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;

Feature: src/modules/upload/upload.routes.ts

import { Router } from "express";
 
import upload from "../../shared/middlewares/upload-file";
import {
  deleteFile,
  uploadFile,
  uploadMultipleFile
} from "./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/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}`);
});

Security Best Practices

  • Enforce file size limits
  • Validate MIME types when required
  • Use private folders for sensitive uploads
  • Never expose Cloudinary API keys/secrets to the client
  • Prefer authenticated routes for uploads
  • Use environment variables for all sensitive configuration

File & Folder Structure

ServerCN

Select a file to view its contents

Installation

npx servercn-cli add file-upload