Загрузка файлов
Файлы отправляют не в JSON, а в формате multipart/form-data — так браузер передаёт формы с полем выбора файла. Fastify по умолчанию его не разбирает, для этого нужен официальный плагин @fastify/multipart. В блоге через него к статье загружается обложка.
Установка и подключение
npm install @fastify/multipart// src/app.js
import multipart from '@fastify/multipart';
app.register(multipart, {
limits: { fileSize: 2 * 1024 * 1024 },
});limits.fileSize ограничивает размер файла двумя мегабайтами. Файл больше лимита сервер не примет и вернёт ошибку 413.
Маршрут загрузки
Создайте в корне проекта папку uploads и добавьте маршрут в 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('Файл не передан');
}
const extension = allowedTypes[file.mimetype];
if (!extension) {
throw app.httpErrors.badRequest('Нужна картинка JPEG или PNG');
}
const fileName = randomUUID() + extension;
await pipeline(file.file, createWriteStream(path.join(uploadsDir, fileName)));
return reply.code(201).send({ url: `/uploads/${fileName}` });
});
}request.file() возвращает первый файл из запроса: file.filename — исходное имя, file.mimetype — тип, file.file — поток с содержимым. pipeline записывает поток в файл на диске, не загружая его целиком в память.
Проверка
curl -X POST http://localhost:3000/posts/1/cover \
-F "cover=@photo.jpg"Флаг -F отправляет форму с файлом. Сервер ответит {"url":"/uploads/....jpg"}, а в папке uploads появится картинка.
Раздача загруженных файлов
Чтобы ссылка открывалась, подключите для uploads ещё один экземпляр @fastify/static:
// src/app.js
app.register(fastifyStatic, {
root: path.join(import.meta.dirname, '..', 'uploads'),
prefix: '/uploads/',
decorateReply: false,
});decorateReply: false нужен, потому что метод reply.sendFile уже добавлен первой регистрацией плагина. Теперь адрес из ответа открывается в браузере.
Поля формы
Обычные текстовые поля из той же формы доступны в file.fields, например file.fields.alt.value. Поля, отправленные до файла, попадают туда гарантированно, поэтому в форме ставьте их раньше поля с файлом.
Частая ошибка
Нельзя сохранять файл под именем, которое прислал клиент. Имя вроде ../src/app.js позволит перезаписать код сервера, а одинаковые имена затрут чужие файлы. Генерируйте имя сами, как randomUUID() в примере, и проверяйте тип файла.