Skip to main content

File Uploads

On the Express stack, Efesto wires Multer into the middleware chain so an endpoint can accept multipart/form-data. You opt in per method with a boolean flag; Efesto handles the rest and exposes the file(s) on the request.

Both stacks

The _<verb>Multer / _<verb>MultipleMulter flags work on both stacks. On Express, Efesto wires Multer and the file arrives in req.file / req.files. On Bun/Elysia, Efesto sets the TypeBox body schema to t.File() / t.Files() and the file arrives in ctx.body (see below). You can also accept files natively on Elysia with your own t.File() body schema.

Single file

Two things are needed: document the body as multipart/form-data, and set the _<method>Multer flag to true. The uploaded file is then available as req.file:

import { BaseApiService, SwaggerOptions } from "efesto";
import type { Request, Response } from "express";

class Documents extends BaseApiService {
constructor() {
super(__filename);
}

_postSwagger: SwaggerOptions = {
operationId: "uploadDocument",
requestBody: {
content: {
"multipart/form-data": {
schema: {
type: "object",
properties: {
file: { type: "string", format: "binary" },
},
},
},
},
},
};

// enable Multer for POST (single file)
_postMulter: boolean = true;

_post(req: Request, res: Response) {
const file = req.file; // the uploaded file (in memory)
return res.json({ name: file?.originalname, size: file?.size });
}
}
export default Documents;

The field name Multer expects for a single upload is file.

Multiple files

Add _<method>MultipleMulter = true alongside the single flag, document the body as an array of binaries, and read req.files:

class Documents extends BaseApiService {
constructor() { super(__filename); }

_postSwagger: SwaggerOptions = {
operationId: "uploadDocuments",
requestBody: {
content: {
"multipart/form-data": {
schema: {
type: "object",
properties: {
files: {
type: "array",
items: { type: "string", format: "binary" },
},
},
},
},
},
},
};

_postMulter: boolean = true;
_postMultipleMulter: boolean = true; // required for multiple files

_post(req: Request, res: Response) {
const files = req.files; // array of uploaded files
return res.json({ count: Array.isArray(files) ? files.length : 0 });
}
}

The field name for multiple uploads is files.

How it behaves

  • Efesto uses Multer's memory storage by default, so files arrive as buffers (req.file.buffer / file.buffer), nothing is written to disk for you.
  • The Multer middleware runs first in the chain, before validation and your handler, so req.file / req.files are populated by the time the method runs.
  • The flags are read by method name: _postMulter, _putMulter, etc., matching the HTTP verb of the endpoint.

On the Bun/Elysia stack

The same flags apply to a class extending BaseApiService. Instead of wiring Multer, Efesto sets the route's TypeBox body schema (t.File() for _<verb>Multer, t.Files() for _<verb>MultipleMulter) when you don't declare one yourself; the file arrives in ctx.body as a web File:

import { BaseApiService, type Context, SwaggerOptions } from "efesto/elysia";

export default class extends BaseApiService {
_putSwagger: SwaggerOptions = {
operationId: "uploadFile",
requestBody: {
content: {
"multipart/form-data": {
schema: { type: "object", properties: { file: "string::binary" } },
},
},
},
};

_putMulter = true; // body schema becomes t.Object({ file: t.File() })

async _put({ body }: Context) {
const file = (body as { file: File }).file;
return file?.name;
}
}

The field names match Express: file for a single upload, files for multiple. The flags exist on post / patch / put only.

Validating and storing files

Efesto gets the bytes to your handler; what you do next is your code. Validate type and size, then persist where you want, all inside the method:

_post(req: Request, res: Response) {
const file = req.file;
if (!file) return res.status(400).json({ error: "No file" });
if (file.size > 5 * 1024 * 1024) return res.status(413).json({ error: "Too large" });
if (!file.mimetype.startsWith("image/")) return res.status(415).json({ error: "Images only" });

// store file.buffer wherever you like: disk, S3, GCS, a database...
const id = persist(file.buffer, file.originalname); // your storage logic
return res.json({ id });
}
Out of scope

Cloud storage (S3, GCS), image processing, compression, and virus scanning are not Efesto features. They are ordinary code you run against file.buffer using the library of your choice.