View as Json

File Upload (ImageKit)

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

Docs:



Installation Guide

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.

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"),
 
  IMAGEKIT_PRIVATE_KEY: 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 an ImageKit configuration file:

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

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

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

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

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