Content Handling
File Uploads
MoroJS provides built-in file upload middleware with validation, type checking, and security features. Easily handle single or multiple file uploads with minimal configuration.
Overview
Handle multipart uploads with type, size, and count validation. Files arrive as buffers on req.files, and land on disk when you give the middleware a destination.
1import { createApp, middleware } from '@morojs/moro';
2
3const app = await createApp();
4
5// Built-in upload middleware
6app.use(middleware.upload({
7 dest: './uploads',
8 maxFileSize: 10 * 1024 * 1024, // 10MB
9 maxFiles: 5,
10 allowedTypes: ['image/jpeg', 'image/png', 'image/gif']
11}));
12
13// Handle file uploads
14app.post('/upload', (req, res) => {
15 const files = req.files;
16
17 if (!files || Object.keys(files).length === 0) {
18 return res.status(400).json({ error: 'No files uploaded' });
19 }
20
21 return {
22 success: true,
23 files: Object.values(files).map((f: any) => ({
24 filename: f.filename,
25 size: f.size,
26 mimetype: f.mimetype
27 }))
28 };
29});The middleware covers parsing, validation, and safe storage. Anything past that — scanning, image or document processing, pushing files to object storage — stays in your hands, with the library you'd pick anyway.
Parsing Multipart By Hand
- Boundary parsing and buffering to write yourself
- Filenames from the client used as-is
- No size or count limits until you add them
- Same-named uploads overwrite each other
With MoroJS
- Parsed and attached to req.files
- Filenames sanitized to a basename
- Configurable size and count limits
- Collision-free names on disk
Validated
MIME type, per-file size, and per-request count checks before anything is stored.
Safe by default
Sanitized filenames and randomized names on disk; rejected files are never written.
In memory or on disk
Buffers on req.files, plus a path when you set a destination.
How It Works
MoroJS includes a built-in upload middleware that handles file uploads with automatic validation, type checking, and security features. The middleware provides a simple API for handling both single and multiple file uploads, with support for file size limits, type restrictions, and custom storage options.
Basic File Upload
MoroJS provides built-in support for file uploads with validation, type checking, and security features.
1import { createApp, middleware } from '@morojs/moro';
2
3const app = await createApp({
4 cors: true,
5 compression: true
6});
7
8// Built-in upload middleware with validation
9app.use(middleware.upload({
10 dest: './uploads',
11 maxFileSize: 10 * 1024 * 1024, // 10MB
12 maxFiles: 5,
13 allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf']
14}));
15
16// Single file upload endpoint
17app.post('/upload', (req, res) => {
18 const files = req.files;
19
20 if (!files || Object.keys(files).length === 0) {
21 return res.status(400).json({
22 success: false,
23 error: 'No file uploaded'
24 });
25 }
26
27 const file = Object.values(files)[0];
28
29 return {
30 success: true,
31 file: {
32 filename: file.filename,
33 originalFilename: file.originalFilename,
34 mimetype: file.mimetype,
35 size: file.size,
36 path: file.path
37 }
38 };
39});
40
41// Multiple file upload endpoint
42app.post('/upload/multiple', (req, res) => {
43 const files = req.files;
44
45 if (!files || Object.keys(files).length === 0) {
46 return res.status(400).json({
47 success: false,
48 error: 'No files uploaded'
49 });
50 }
51
52 const fileList = Object.values(files).map((file: any) => ({
53 filename: file.filename,
54 originalFilename: file.originalFilename,
55 mimetype: file.mimetype,
56 size: file.size,
57 path: file.path
58 }));
59
60 return {
61 success: true,
62 files: fileList,
63 count: fileList.length
64 };
65});Automatic Features
- File type validation based on MIME types
- Per-file size and per-request count limits
- Filename sanitization
- Collision-free names on disk when dest is set
Advanced Configuration
1import { createApp, middleware } from '@morojs/moro';
2
3const app = await createApp();
4
5app.use(middleware.upload({
6 dest: './uploads', // optional; written to disk when set
7 maxFileSize: 10 * 1024 * 1024, // bytes, per file (default 5MB)
8 maxFiles: 5, // per request (default 10)
9 allowedTypes: ['image/jpeg', 'image/png'] // mimetypes; omit to allow any
10}));
11
12// Every uploaded file
13app.post('/upload', (req, res) => {
14 for (const file of Object.values(req.files ?? {})) {
15 file.filename; // sanitized to a basename
16 file.originalFilename;
17 file.mimetype;
18 file.size;
19 file.data; // contents, in memory
20 file.path; // absolute path on disk — only when dest is set
21 file.destination; // the directory it was written to
22 }
23
24 return { received: Object.keys(req.files ?? {}).length };
25});How storage works
- Without dest, nothing touches disk — files are available as in-memory buffers on file.data
- With dest, each file is written under a randomized name, so two uploads of the same filename cannot overwrite each other
- Filenames are reduced to a basename with control characters stripped, so a crafted name cannot escape the destination
- Files that fail validation are rejected with a 400 and never written
- Options apply to the whole request — there is no per-field configuration, and no built-in cloud storage, malware scanning, or image processing
Multiple File Uploads
1// Multiple files schema
2const MultipleFilesSchema = z.object({
3 files: z.array(z.custom<File>((val) => val instanceof File))
4 .min(1, 'At least one file is required')
5 .max(5, 'Maximum 5 files allowed'),
6 category: z.enum(['documents', 'images', 'videos'])
7});
8
9app.post('/upload/multiple')
10 .body(MultipleFilesSchema)
11 .handler(async ({ body }) => {
12 const { files, category } = body;
13
14 const results = await Promise.all(
15 files.map(async (file, index) => {
16 try {
17 const savedPath = await saveFile(file, {
18 directory: `uploads/${category}`,
19 rename: `${Date.now()}_${index}_${file.name}`
20 });
21
22 return {
23 success: true,
24 originalName: file.name,
25 savedPath,
26 size: file.size,
27 type: file.type
28 };
29 } catch (error) {
30 return {
31 success: false,
32 originalName: file.name,
33 error: error.message
34 };
35 }
36 })
37 );
38
39 const successful = results.filter(r => r.success);
40 const failed = results.filter(r => !r.success);
41
42 return {
43 uploaded: successful.length,
44 failed: failed.length,
45 results
46 };
47 }));Cloud Storage Integration
1import { createApp, middleware } from '@morojs/moro';
2import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
3
4// MoroJS writes uploads to a local directory. Push them to your
5// provider yourself with that provider's SDK.
6const app = await createApp();
7const s3 = new S3Client({ region: process.env.S3_REGION });
8
9app.use(middleware.upload({
10 dest: './uploads',
11 maxFileSize: 10 * 1024 * 1024,
12 allowedTypes: ['image/jpeg', 'image/png']
13}));
14
15app.post('/upload/cloud', async (req, res) => {
16 const [file] = Object.values(req.files ?? {});
17 if (!file) return res.status(400).json({ error: 'No file uploaded' });
18
19 const key = `users/${req.auth?.user?.id}/${file.filename}`;
20
21 await s3.send(new PutObjectCommand({
22 Bucket: process.env.S3_BUCKET,
23 Key: key,
24 Body: file.data,
25 ContentType: file.mimetype,
26 ServerSideEncryption: 'AES256'
27 }));
28
29 res.json({ success: true, key });
30});File Processing
1import sharp from 'sharp';
2
3// MoroJS has no image pipeline — uploaded files arrive as Buffers on
4// req.files, so hand them to an image library of your choice.
5app.post('/upload/image', async (req, res) => {
6 const [image] = Object.values(req.files ?? {});
7 if (!image?.mimetype.startsWith('image/')) {
8 return res.status(400).json({ error: 'Expected an image' });
9 }
10
11 const resized = await sharp(image.data)
12 .resize({ width: 1920, height: 1080, fit: 'inside', withoutEnlargement: true })
13 .jpeg({ quality: 85, progressive: true })
14 .toBuffer();
15
16 const thumbnail = await sharp(image.data).resize(320).toBuffer();
17
18 res.json({
19 original: image.filename,
20 size: resized.length,
21 thumbnailSize: thumbnail.length
22 });
23});1// Documents work the same way: MoroJS validates and stores them,
2// anything beyond that is your own library of choice.
3app.use(middleware.upload({
4 dest: './uploads',
5 allowedTypes: ['application/pdf', 'text/plain']
6}));
7
8app.post('/upload/document', async (req, res) => {
9 const [doc] = Object.values(req.files ?? {});
10 if (!doc) return res.status(400).json({ error: 'No document uploaded' });
11
12 // doc.path points at the stored file; doc.data holds the bytes
13 const text = await extractTextWithYourParser(doc.path);
14
15 return { filename: doc.filename, size: doc.size, wordCount: text.split(/\s+/).length };
16});Security Best Practices
What the middleware does for you
- MIME type validation against allowedTypes
- Per-file size limit and per-request file count limit
- Filenames reduced to a basename with control characters stripped
- Randomized names on disk, so uploads cannot overwrite each other
- Rejected files are never written
What is still yours to do
- MIME types come from the client — verify content yourself if it matters
- There is no malware scanning, quarantine, or metadata stripping
- Store uploads outside the directory you serve statically
- Rate limit upload endpoints
- Log upload activity
1import { createApp, middleware } from '@morojs/moro';
2
3const app = await createApp();
4
5// Uploads land outside ./public, so nothing is served straight back
6app.use(middleware.upload({
7 dest: './var/uploads',
8 maxFileSize: 5 * 1024 * 1024,
9 maxFiles: 1,
10 allowedTypes: ['image/jpeg', 'image/png']
11}));
12
13app.post('/upload')
14 .rateLimit({ requests: 5, window: 60000 })
15 .handler((req, res) => {
16 const [file] = Object.values(req.files ?? {});
17 if (!file) return res.status(400).json({ error: 'No file uploaded' });
18
19 // file.mimetype is the client's claim — check the bytes before trusting it
20 return { stored: file.path, size: file.size };
21 });