const multer = require('multer');
const ALLOWED_MIME = new Set(['image/jpeg', 'image/png', 'image/webp']);
function fileFilter(req, file, cb) {
if (!ALLOWED_MIME.has(file.mimetype)) {
const err = new Error('Only JPEG, PNG, or WebP images are allowed');
err.code = 'INVALID_FILE_TYPE';
return cb(err, false);
}
cb(null, true);
}
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 5 * 1024 * 1024, // 5 MB
files: 1,
},
fileFilter,
});
const avatarUpload = upload.single('avatar');
module.exports = { avatarUpload };
const path = require('path');
const crypto = require('crypto');
const sharp = require('sharp');
const SIZES = [
{ label: 'thumb', size: 64 },
{ label: 'full', size: 256 },
];
async function processAvatar(buffer, outputDir) {
const baseName = crypto.randomBytes(16).toString('hex');
const pipeline = sharp(buffer).rotate(); // honor EXIF orientation
const variants = {};
for (const { label, size } of SIZES) {
const filename = `${baseName}-${label}.webp`;
const outputPath = path.join(outputDir, filename);
await pipeline
.clone()
.resize(size, size, { fit: 'cover', position: 'centre' })
.webp({ quality: 82 })
.toFile(outputPath);
variants[label] = { filename, path: outputPath, size };
}
return { baseName, variants };
}
module.exports = { processAvatar };
const path = require('path');
const express = require('express');
const { avatarUpload } = require('./upload');
const { processAvatar } = require('./imageProcessor');
const User = require('./models/User');
const router = express.Router();
const AVATAR_DIR = path.join(__dirname, '..', 'public', 'avatars');
router.post('/me/avatar', avatarUpload, async (req, res, next) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No avatar file provided' });
}
const { baseName, variants } = await processAvatar(req.file.buffer, AVATAR_DIR);
await User.updateOne(
{ _id: req.user.id },
{ $set: { avatarBaseName: baseName } }
);
res.status(201).json({
thumb: `/avatars/${variants.thumb.filename}`,
full: `/avatars/${variants.full.filename}`,
});
} catch (err) {
next(err);
}
});
router.use((err, req, res, next) => {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'Avatar must be 5 MB or smaller' });
}
if (err.code === 'INVALID_FILE_TYPE') {
return res.status(400).json({ error: err.message });
}
next(err);
});
module.exports = router;
This snippet shows a complete, focused avatar upload flow in Express: accepting a multipart file, validating it, resizing it into a couple of standard sizes, and persisting the results. The work is split so that each concern lives where it belongs — Multer configuration and guards, an image-processing helper, and the route that ties them together.
In upload.js, Multer is configured with memoryStorage() rather than writing to disk. Buffering in memory is the natural choice when the file is going to be piped straight into a resizer, because it avoids a temp-file round trip and lets the processing step own where the final bytes land. limits caps the payload at 5 MB so a huge upload is rejected before it is fully read, and fileFilter rejects anything whose MIME type is not in ALLOWED_MIME. The filter passes a typed error (code: 'INVALID_FILE_TYPE') so the route can translate it into a clean 400 later. Exposing avatarUpload as upload.single('avatar') keeps the route declaration terse.
In imageProcessor.js, processAvatar wraps sharp to produce derivatives from the in-memory buffer. sharp(buffer).rotate() first normalizes EXIF orientation — a common pitfall where phone photos appear sideways because the orientation is stored as metadata rather than baked into pixels. It then loops over SIZES, using resize with fit: 'cover' and position: 'centre' to crop to a square, re-encoding as WebP for smaller files. Each variant is written under a random baseName so filenames never collide and can't be guessed. Returning a plain descriptor object keeps the helper storage-agnostic and easy to test.
In avatarRoutes.js, the pieces compose: avatarUpload runs first, then the handler checks req.file exists, calls processAvatar, updates the user, and returns the variant URLs. A trailing error middleware inspects err.code to map LIMIT_FILE_SIZE and INVALID_FILE_TYPE to 400s, so Multer's internal errors surface as meaningful API responses instead of a generic 500.
The trade-off of memory storage is RAM pressure under high concurrency; for very large files a streaming or disk-backed approach is safer. For typical avatars, this pattern is simple, fast, and keeps validation, processing, and routing cleanly separated.
Related snips
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
import cv2
image = cv2.imread('receipt.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
thresholded = cv2.adaptiveThreshold(
OpenCV image preprocessing for OCR and vision pipelines
# Installation
# rails active_storage:install
# rails db:migrate
# config/storage.yml
local:
ActiveStorage for file uploads and attachments
import type { IncomingMessage, ServerResponse } from "http";
const MIN_BYTES = 1024;
const INCOMPRESSIBLE = /^(image|video|audio)\/|application\/(zip|gzip|x-brotli|pdf|octet-stream)/i;
Response compression (only when it helps)
Share this code
Here's the card — post it anywhere.