File uploads
Files are sent not as JSON but in the multipart/form-data format — the way a browser sends forms with a file input. Fastify does not parse it by default; the official @fastify/multipart plugin is needed. In the blog it is used to upload a post cover image.
Installing and registering
npm install @fastify/multipart// src/app.js
import multipart from '@fastify/multipart';
app.register(multipart, {
limits: { fileSize: 2 * 1024 * 1024 },
});limits.fileSize limits a file to two megabytes. The server refuses a larger file and returns a 413 error.
The upload route
Create an uploads folder in the project root and add a route to src/routes/posts.js:
// src/routes/posts.js
import { createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { randomUUID } from 'node:crypto';
import path from 'node:path';
const uploadsDir = path.join(import.meta.dirname, '..', '..', 'uploads');
const allowedTypes = { 'image/jpeg': '.jpg', 'image/png': '.png' };
export default async function postRoutes(app) {
app.post('/posts/:id/cover', async (request, reply) => {
const file = await request.file();
if (!file) {
throw app.httpErrors.badRequest('No file sent');
}
const extension = allowedTypes[file.mimetype];
if (!extension) {
throw app.httpErrors.badRequest('A JPEG or PNG image is required');
}
const fileName = randomUUID() + extension;
await pipeline(file.file, createWriteStream(path.join(uploadsDir, fileName)));
return reply.code(201).send({ url: `/uploads/${fileName}` });
});
}request.file() returns the first file of the request: file.filename is the original name, file.mimetype the type, file.file a stream with the contents. pipeline writes the stream to a file on disk without loading it all into memory.
Trying it
curl -X POST http://localhost:3000/posts/1/cover \
-F "cover=@photo.jpg"The -F flag sends a form with a file. The server answers {"url":"/uploads/....jpg"}, and the picture appears in the uploads folder.
Serving uploaded files
To make the link open, register one more @fastify/static instance for uploads:
// src/app.js
app.register(fastifyStatic, {
root: path.join(import.meta.dirname, '..', 'uploads'),
prefix: '/uploads/',
decorateReply: false,
});decorateReply: false is needed because the reply.sendFile method was already added by the first registration. Now the address from the reply opens in a browser.
Form fields
Ordinary text fields of the same form are available in file.fields, for example file.fields.alt.value. Fields sent before the file are guaranteed to be there, so put them before the file input in the form.
Common mistake
Never save a file under the name the client sent. A name such as ../src/app.js could overwrite the server code, and identical names overwrite other people's files. Generate the name yourself, as randomUUID() does in the example, and check the file type.