
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 може зробити більше. Не краще: що насправді кажуть дослідження про розробку ігор
Generative AI входить у production ігор, але докази складніші за хайп. Розбираємо adoption, ставлення гравців, ризики якості та практичний workflow.

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 допомагає брендам перемагати у «агентному» вебі.
Професійна розробка для вашого бізнесу
Створюємо сучасні веб-рішення та боти для бізнесу. Дізнайтеся, як ми можемо допомогти вам досягти цілей.