
Bun.js + S3 + Sharp: завантаження та оптимізація зображень
Як побудувати безпечний image pipeline на Bun.js: presigned URL для S3-compatible storage, Sharp для resize/WebP та валідація файлів без перевантаження API.
API на Bun спершу перевіряє тип і розмір майбутнього файла, видає короткоживучий presigned PUT URL, а браузер завантажує файл напряму в S3. Окремий worker або endpoint читає оригінал, створює WebP/thumbnail через Sharp і зберігає похідні об'єкти.
Архітектура без зайвого проксіювання файлів
Bun залишається control plane, а object storage — data plane.
01
Підготувати upload
Клієнт надсилає filename, MIME type і розмір. API перевіряє allowlist та повертає key і presigned URL.
02
Завантажити напряму
Браузер виконує PUT у S3. Ваш Bun-процес не тримає байти у RAM і не стає вузьким місцем.
03
Обробити
Worker або внутрішній endpoint читає оригінал, Sharp створює thumbnail і webp-версію.
04
Опублікувати
У базі зберігаються тільки перевірені ключі, розміри, MIME type і статус обробки.
Файл іде напряму в storage, тому Bun обробляє контроль доступу та метадані, а не проксіює великі байти.
Скріншот секції architectureКрок 1: встановлюємо AWS SDK та Sharp
Додайте залежності
bun add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner sharpSharp містить native-компонент. Перевірте, що target-платформа вашого Docker-образу відповідає платформі, для якої встановлено optional dependencies.
Задайте bucket
S3_REGION=eu-central-1
S3_BUCKET=media
S3_ENDPOINT=https://s3.example.com
S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...S3_ENDPOINT можна вказати для Cloudflare R2 або MinIO. Секретний ключ ніколи не відправляйте у frontend.
Крок 2: видаємо presigned PUT URL
URL має жити недовго, містити випадковий object key і бути прив'язаним до очікуваного Content-Type. Для production також перевіряйте розмір після upload через HEAD або подію storage.
// src/storage.ts
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({
region: Bun.env.S3_REGION!,
endpoint: Bun.env.S3_ENDPOINT || undefined,
forcePathStyle: Boolean(Bun.env.S3_ENDPOINT),
});
const allowed = new Set(["image/jpeg", "image/png", "image/webp"]);
export async function createUploadUrl(contentType: string) {
if (!allowed.has(contentType)) throw new Error("Unsupported image type");
const key = `originals/${crypto.randomUUID()}`;
const command = new PutObjectCommand({
Bucket: Bun.env.S3_BUCKET!,
Key: key,
ContentType: contentType,
ServerSideEncryption: "AES256",
});
return { key, url: await getSignedUrl(s3, command, { expiresIn: 300 }) };
}П'ять хвилин достатньо для звичайного upload і зменшує вікно для зловживань.
Маршрут підготовки upload і callback після обробки
API не має вважати файл готовим одразу після видачі URL. Створіть запис зі статусом pending, а після успішного PUT або повідомлення worker-а переведіть його в ready.
// src/routes/uploads.ts
import { Elysia, t } from "elysia";
import { createUploadUrl } from "../storage";
export const uploadRoutes = new Elysia({ prefix: "/uploads" })
.post("/prepare", async ({ body, set }) => {
const upload = await createUploadUrl(body.contentType);
// Збережіть upload.key і userId у БД зі статусом pending.
set.status = 201;
return { ...upload, status: "pending" };
}, {
body: t.Object({
contentType: t.Union([t.Literal("image/jpeg"), t.Literal("image/png"), t.Literal("image/webp")]),
}),
});На frontend: PUT у повернутий URL з точним Content-Type, потім повідомлення POST /uploads/:id/complete. Worker повторно перевіряє object metadata перед Sharp.
Не приймайте від клієнта готову публічну URL-адресу. Публікуйте лише key, який створив сервер, і будуйте CDN URL централізовано.
Безпека та контроль витрат
Upload endpoint — це не просто форма. Обмеження мають бути на кожному етапі.
Обмежуйте розмір
Лімітуйте payload на API та перевіряйте фактичний Content-Length/розмір object після PUT.
Не приймайте довільні ключі
Генеруйте key на сервері з UUID і namespace користувача або проєкту.
Скануйте або модеруйте
Для публічного контенту додайте antivirus/moderation крок до зміни статусу на ready.
Додавайте lifecycle rules
Тимчасові originals і незавершені uploads мають автоматично видалятися через storage lifecycle.
Не робіть Sharp у request без ліміту
Важку обробку краще винести в чергу або окремий worker, щоб upload API залишався responsive.
Мінімальний тест для Sharp pipeline
Тестуйте не тільки HTTP-статус, а й результат декодування: формат, ширину та приблизний розмір. Так ви помітите випадкову зміну quality або зламану native-залежність після оновлення Docker image.
import { expect, test } from "bun:test";
import sharp from "sharp";
import { makeVariants } from "./images";
test("creates a bounded WebP thumbnail", async () => {
const input = await sharp({
create: { width: 1200, height: 800, channels: 3, background: "#f97316" },
}).png().toBuffer();
const { thumbnail } = await makeVariants(input);
const meta = await sharp(thumbnail).metadata();
expect(meta.format).toBe("webp");
expect(meta.width).toBe(480);
expect(meta.height).toBe(480);
});Запуск: bun test --coverage. Для integration-тесту storage використовуйте MinIO у Compose або окремий тестовий bucket з lifecycle cleanup.
Коли обрати цей підхід
Підходить
Аватари, каталоги, CMS, user-generated content і будь-які файли, де потрібні thumbnail та CDN-friendly WebP.
Потребує іншої схеми
Якщо потрібні відео, десятки гігабайтів або realtime-прогрес обробки, додайте чергу, multipart upload і спеціалізований media worker.
Часті запитання
Ні. Для великих файлів краще видавати presigned URL і завантажувати напряму в S3-compatible storage. Bun керує дозволом, метаданими та статусом обробки.
Так, але Sharp має native-залежності, тому їх потрібно коректно встановити для цільової ОС і архітектури. Перевіряйте production Docker image окремим smoke test.
Висновок
Опишіть задачу — перші 15 хвилин консультації безкоштовні.
Пов'язані статті

Скільки коштує розробка AI асистента у 2026: RAG чатбот, база знань, CRM, Telegram та підтримка
Практичний гід для бізнесу: від чого залежить ціна розробки AI асистента у 2026 році, що входить у RAG чатбот, інтеграції з CRM, Telegram, guardrails, оцінювання, моніторинг і супровід.

AI для розробки лендінгів: де він реально прискорює запуск, а де псує конверсію
Дослідження про використання AI у розробці лендінгів: v0, Webflow AI, Builder.io, Framer-подібні AI builders, генерація UX, copy, SEO, персоналізація, A/B тести, ризики шаблонності, безпеки, доступності та технічного боргу.

AI SEO / GEO у 2026: ваші наступні клієнти — не люди, а агенти
Пошук зміщується від кліків до відповідей. Боти та AI-агенти сканують, цитують, рекомендують і дедалі частіше купують. Дізнайтесь, що таке AI SEO / GEO, чому класичного SEO вже недостатньо, і як PAS7 Studio допомагає брендам перемагати у «агентному» вебі.

Найпотужніший чіп від Apple? M5 Pro і M5 Max б'ють рекорди
Аналітичний розбір Apple M5 Pro і M5 Max станом на березень 2026 року. Пояснюємо, чому ці чіпи можна вважати найпотужнішими професійними ноутбучними SoC від Apple, як вони виглядають на тлі M4 Pro, M4 Max, M1 Pro, M1 Max і що показують у порівнянні з актуальними Intel та AMD.
Професійна розробка для вашого бізнесу
Створюємо сучасні веб-рішення та боти для бізнесу. Дізнайтеся, як ми можемо допомогти вам досягти цілей.