File Upload (ImageKit)

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

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

Official ImageKit Documentation, ImageKit Node.js SDK

Features

  • ImageKit-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 an ImageKit account. Click here if you don't have one.

Define the following environment variables:

PORT="8000"
NODE_ENV="development"
LOG_LEVEL="info"
 
# ImageKit Configuration
IMAGEKIT_PRIVATE_KEY="your-imagekit-private-key"

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;
 
  IMAGEKIT_PRIVATE_KEY: string;
}
 
const env: Config = {
  PORT: Number(process.env.PORT) || 3000,
  NODE_ENV: process.env.NODE_ENV || "development",
  LOG_LEVEL: process.env.LOG_LEVEL || "info",
 
  IMAGEKIT_PRIVATE_KEY: process.env.IMAGEKIT_PRIVATE_KEY!
};
 
export default env;

Basic Implementation

Create an ImageKit configuration file:

MVC: src/configs/imagekit.ts

Feature: src/shared/configs/imagekit.ts

import ImageKit from '@imagekit/nodejs';
import env from './env';
 
const imagekitClient = new ImageKit({
  privateKey: env.IMAGEKIT_PRIVATE_KEY,
});
 
export default imagekitClient;

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 ImageKit and deleting files from ImageKit.

MVC: src/services/upload.service.ts or src/utils/imagekit.ts

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

import imagekitClient from "../configs/imagekit";
import { toFile } from "@imagekit/nodejs";
 
export interface UploadOptions {
  folder: string;
  fileName?: string;
}
 
export interface ImageKitUploadResult {
  url: string;
  fileId: string;
  size: number;
}
 
export const uploadToImageKit = async (
  buffer: Buffer,
  options: UploadOptions
): Promise<ImageKitUploadResult> => {
  try {
    const fileName = options.fileName || `file-${Date.now()}`;
    const file = await toFile(buffer, fileName);
 
    const result = await imagekitClient.files.upload({
      file: file,
      fileName: fileName,
      folder: options.folder || "uploads"
    });
 
    return {
      url: result.url || "",
      fileId: result.fileId || "",
      size: result.size || 0
    };
  } catch (error) {
    throw error;
  }
};
 
export const deleteFileFromImageKit = async (
  fileIds: string[]
): Promise<void> => {
  try {
    await Promise.all(
      fileIds.map(fileId => imagekitClient.files.delete(fileId))
    );
  } catch (error) {
    throw error;
  }
};

Usage Example

MVC: src/controllers/upload.controller.ts

import { Request, Response, NextFunction } from "express";
 
import {
  ImageKitUploadResult,
  deleteFileFromImageKit,
  uploadToImageKit
} from "../services/upload.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 uploadToImageKit(req.file.buffer, {
      folder: "uploads/files",
      fileName: req.file.originalname
    });
 
    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: ImageKitUploadResult[] = await Promise.all(
      files.map(async file => {
        return await uploadToImageKit(file.buffer, {
          folder: "uploads/images",
          fileName: file.originalname
        });
      })
    );
 
    return ApiResponse.created(res, "Files uploaded successfully", results);
  }
);
 
export const deleteFile = AsyncHandler(
  async (req: Request, res: Response, next: NextFunction) => {
    const { fileIds }: { fileIds: string[] } = req.body;
 
    if (!fileIds || fileIds.length === 0) {
      return next(ApiError.badRequest("File IDs are required"));
    }
 
    await deleteFileFromImageKit(fileIds);
 
    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 uploadToImageKit(req.file.buffer, {
      folder: "uploads/files",
      fileName: req.file.originalname
    });
 
    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: ImageKitUploadResult[] = await Promise.all(
      files.map(async file => {
        return await uploadToImageKit(file.buffer, {
          folder: "uploads/images",
          fileName: file.originalname
        });
      })
    );
 
    return ApiResponse.created(res, "Files uploaded successfully", results);
  }
);
 
export const deleteFile = AsyncHandler(
  async (req: Request, res: Response, next: NextFunction) => {
    const { fileIds }: { fileIds: string[] } = req.body;
 
    if (!fileIds || fileIds.length === 0) {
      return next(ApiError.badRequest("File IDs are required"));
    }
 
    await deleteFileFromImageKit(fileIds);
 
    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 ImageKit private keys 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