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.

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.

👉 Note: You do not need to install any servercn dependencies manually. Installing this component will automatically install all required servercn dependencies. Manual installation is optional if you prefer to manage dependencies yourself.

  • HTTP Status Codes:
npx servercn add http-status-codes

Documentation: HTTP Status Codes

  • Api Response Handler:
npx servercn add response-formatter

Documentation: Api Response Handler

  • Api Error Handler:
npx servercn add error-handler

Documentation: Api Error Handler

  • Logger:
npx servercn add logger

Documentation: Logger

  • Async Handler:
npx servercn add async-handler

Documentation: Async Handler

  • Global Error Handler:
npx servercn add global-error-handler

Documentation: Global Error Handler

npx servercn add file-upload

Prerequisites

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

Define the following environment variables:

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

Ensure the following configuration are defined:

src/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:

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 delete files from cloudinary.

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

src/helpers/cloudinary.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

src/controllers/upload.controller.ts
import { Request, Response } 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) => {
  if (!req.file) {
    throw 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) => {
    const files = req.files as Express.Multer.File[];
 
    if (!files || files.length === 0) {
      throw 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) => {
  const { public_id } = req.body;
 
  if (!public_id) {
    throw 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/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 secrets to the client
  • Prefer authenticated routes for uploads

File & Folder Structure

Select a file to view its contents

Installation

npx servercn add file-upload