@freshpointcz/fresh-s3
v0.0.1
Published
Reusable, strongly-typed S3 abstraction
Keywords
Readme
@freshpointcz/fresh-s3 🪣
A strongly-typed, fail-loud S3 abstraction built for the internal microservices ecosystem.
This package wraps the AWS SDK v3 with a tiny, opinionated API for uploading, downloading, and listing objects. It works against Hetzner Object Storage, AWS S3, MinIO, and any S3-compatible provider — a service just imports the client, points it at a bucket, and calls a handful of methods.
✨ Key Features
- Fail-Loud By Design: Every operation either returns a typed result or throws a concrete
S3Errorsubclass. Nothing is ever silently swallowed — nonull, nofalse, no quietcatch. - Stream-First Uploads: Uploads always use multipart streaming via
lib-storage, handling objects of any size without buffering them in memory. - Complete Listings:
listManytransparently follows pagination, so results are complete regardless of object count (no silent 1000-object cap). - Typed Errors: Seven discriminated error classes (
S3NotFoundError,S3UploadError, …) carry the originalcauseand the S3 HTTP status. - Domain-Agnostic Folders: The
S3Folderbuilder validates and composes keys without the package ever knowing your concrete folders. - One-Line Setup:
FreshS3Client.fromEnv()reads canonicalS3_*variables — no boilerplate in the service.
📦 Installation
This package declares the AWS SDK packages as peer dependencies, so each service installs the versions it already uses.
npm install @freshpointcz/fresh-s3 \
@aws-sdk/client-s3 \
@aws-sdk/lib-storage \
@aws-sdk/s3-request-presigner⚙️ Configuration
The client needs an endpoint, region, credentials, and a bucket. Provide them explicitly, or read the canonical environment variables.
| Variable | Required | Description |
| ---------------------- | -------- | -------------------------------------------------------- |
| S3_ENDPOINT | yes | Full endpoint URL, e.g. https://fsn1.your-objectstorage.com |
| S3_REGION | yes | Region, e.g. fsn1 |
| S3_ACCESS_KEY_ID | yes | Access key ID |
| S3_SECRET_ACCESS_KEY | yes | Secret access key |
| S3_BUCKET | yes | Target bucket |
| S3_FORCE_PATH_STYLE | no | "true" for MinIO / local path-style storage; otherwise omit |
Per-service credentials: each service gets its own access keys, configured through that service's own environment — never a shared key.
A missing or blank required field throws S3ConfigError immediately when the client is constructed — misconfiguration never reaches the network.
🚀 Quick Start
import { FreshS3Client, S3Folder } from "@freshpointcz/fresh-s3";
import { Readable } from "stream";
// One line of setup — reads the S3_* environment variables.
const s3 = FreshS3Client.fromEnv();
// Define your folders once (the package stays domain-agnostic).
const PRODUCT = new S3Folder("common", "product");
// Upload (always a stream — wrap a Buffer with Readable.from).
const { key, etag } = await s3.upload(
PRODUCT.key("1778_abc.jpg"),
Readable.from(buffer),
{ contentType: "image/jpeg", acl: "public-read" }
);
// Download as a stream.
const stream = await s3.download(key);
// Presigned, time-limited read URL.
const url = await s3.getSignedUrl(key, 3600);Prefer explicit config over environment variables? Pass it to the constructor:
const s3 = new FreshS3Client({
endpoint: "https://fsn1.your-objectstorage.com",
region: "fsn1",
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
bucket: "fresh-media-prod",
});📚 API
| Method | Returns | Notes |
| --------------------------------------- | ------------------------ | ------------------------------------------------------------ |
| upload(key, body, options?) | S3UploadResult | Multipart stream upload. Body is always a Readable. |
| download(key) | Readable | Throws S3NotFoundError if the object is missing. |
| listOne(key) | S3ObjectMeta | Metadata only (HEAD). Throws S3NotFoundError if missing. |
| listMany(prefix) | S3ObjectMeta[] | Auto-paginates; complete regardless of count. Newest first. |
| delete(key) | void | No-op on S3 for a missing key. |
| getSignedUrl(key, expiresInSeconds?) | string | Default lifetime 3600s. |
| checkConnection() | S3ConnectionStatus | Status report — never throws for an unreachable bucket. |
| verifyConnection() | void | Fail-loud startup variant — throws S3ConnectionError. |
🗂️ Folders
S3Folder builds and validates object keys. It knows the concept of a folder, not your concrete folders — you define those:
const PRODUCT = new S3Folder("common", "product");
PRODUCT.prefix; // "common/product"
PRODUCT.key("foto.jpg"); // "common/product/foto.jpg"
const THUMBS = PRODUCT.child("thumbnails"); // "common/product/thumbnails"Segments are validated on construction: no empty values, no slashes inside a segment, no surrounding whitespace, no ./.. traversal. An invalid segment throws S3ConfigError.
🛑 Error Handling
The library is fail-loud: every method either returns a typed result or throws. All errors extend the abstract S3Error and carry a code, the originating cause, and the S3 httpStatusCode when available.
| Error | code | Thrown when |
| -------------------- | ------------- | -------------------------------------------- |
| S3ConfigError | config | Config or folder segment is invalid |
| S3ConnectionError | connection | Bucket unreachable / credentials rejected |
| S3UploadError | upload | Upload fails |
| S3DownloadError | download | Download or URL signing fails |
| S3NotFoundError | not-found | Object does not exist |
| S3DeleteError | delete | Delete request fails |
| S3ListError | list | Listing or metadata read fails |
import { S3NotFoundError } from "@freshpointcz/fresh-s3";
try {
await s3.download("missing/key.jpg");
} catch (err) {
if (err instanceof S3NotFoundError) {
// handle missing object
}
throw err;
}Note: Since fresh-s3 is transport-agnostic (it may run in a CRON job or CLI, not just an HTTP controller), it does not depend on fresh-core and does not throw ApiError. In an HTTP context, map an S3Error onto your service's own error type in the controller.
🩺 Health Checks
// Health endpoint — reports status, never throws for an unreachable bucket.
const status = await s3.checkConnection();
// → { ok: true } | { ok: false, reason: "..." }
// Startup — fail-loud, aborts boot if S3 is unreachable.
await s3.verifyConnection(); // throws S3ConnectionError