s3proxy
v4.2.1
Published
Streaming web proxy for AWS S3
Maintainers
Readme
s3proxy
Stream files directly from AWS S3 to your users without downloading them to your server first.
s3proxy turns an S3 bucket into a web server. Use it to serve static websites, file downloads, or media directly from S3 while keeping control over access, headers, and routing.
Why s3proxy?
- Zero server storage: files stream directly from S3 to users
- Low latency: no intermediate downloads or disk buffering
- Cost effective: reduce server storage and bandwidth costs
- Scalable: backed by S3
- Range requests: partial content and resumable downloads
- Framework support: Express, Fastify, Hono, and plain
node:http - TypeScript: typed API, ESM-only
- Large files: suited to AI models, datasets, and media assets
Quick Start
Breaking changes in v4.0
v4 fixes the v3 error contract and replaces the response-mutating
proxy.get(req, res) / proxy.head(req, res) / proxy.healthCheckStream(res)
wrappers with a single pure entry point: proxy.fetch(req).
// v3.x: wrapper writes res headers as a side effect, swallows 404 as an empty 200
const stream = await proxy.get(req, res);
stream.pipe(res);
// v4: pure fetch, you write the response
const { stream, status, headers } = await proxy.fetch(req);
res.writeHead(status, headers);
stream.pipe(res);Missing keys, AccessDenied, and InvalidRange now throw typed
S3NotFound / S3Forbidden / S3InvalidRange errors instead of
silently returning empty 200 streams. See MIGRATION.md
for the full v3 to v4 migration guide.
ESM-only since v3. No CommonJS support. For CommonJS projects, add
"type": "module" to your package.json or use dynamic import:
const { S3Proxy } = await import('s3proxy');
Requirements
- Node.js: 22.13.0 or higher
- Package Type: ESM-only (no CommonJS support)
- AWS SDK: v3 (included as dependency)
Installation & Usage
npm install s3proxy expressimport express from 'express';
import { S3Proxy } from 's3proxy';
const app = express();
const proxy = new S3Proxy({ bucket: 'your-bucket-name' });
await proxy.init();
app.get('/*splat', async (req, res) => {
try {
const { stream, status, headers } = await proxy.fetch(req);
res.writeHead(status, headers);
stream.on('error', () => res.end()).pipe(res);
} catch (err) {
res.status(err.statusCode || 500).end();
}
});
app.listen(3000);Express 5 path syntax. The
/*splatwildcard is Express 5'spath-to-regexprequirement; on Express 4 use'/*'. Mixing the two forms crashes at route registration.
Simpler: proxy.middleware()
If you don't need a custom error page, skip the try/catch.
proxy.middleware() is a handler built on fetch() that writes the
status, headers, and body for you. Missing, forbidden, and bad-range
requests return the real 404/403/416 status instead of an empty 200:
app.get('/*splat', proxy.middleware());Or write to a response yourself in one call with proxy.pipe(req, res):
app.get('/*splat', (req, res) => proxy.pipe(req, res));Both resolve when the body finishes streaming, and neither rejects on a
classified S3 failure; they write the correct status instead. Use the
explicit fetch() + try/catch form (below) when you need a custom error
body (XML/HTML), structured logging, or a non-Express framework.
Static website hosting: proxy.staticSite()
proxy.staticSite() reproduces the core of S3 static website hosting
(index documents and a custom error page) on a private bucket, with your
app in front of it:
// / and /dir/ resolve to index.html; missing or forbidden keys serve
// 404.html with the correct 4xx status.
const site = proxy.staticSite({ indexDocument: 'index.html', errorDocument: '404.html' });
app.use((req, res) => site(req, res)); // app.use so `/` matches too| Request | Served |
|------------------------|-----------------------------------------------|
| / | index.html (200) |
| /blog/ | blog/index.html (200) |
| /style.css | style.css (200) |
| missing / forbidden | 404.html with the original 404/403 status |
indexDocument defaults to 'index.html' (set it to '' to disable).
errorDocument is optional; without it, 404 and 403 return a plaintext
status. If the error document is itself missing, staticSite falls back
to a bare status instead of looping. The core fetch() is unchanged and
still throws typed errors. Full example:
examples/static-site.ts.
Behavior:
- Handles GET and HEAD, and passes Range requests through (a range still
returns 206 with
Content-Range). - Forwards the object's S3 response headers:
content-type,cache-control,etag,x-amz-meta-*, and so on. - Serves the error document for missing (404) and access-denied (403) keys, keeping the original status.
Limitations (differences from an S3 website endpoint):
- A path without a trailing slash is fetched as a literal key.
staticSitedoes not issue S3's/photosto/photos/directory redirect, which would need an extra lookup. - Only 404 and 403 use the error document. A 416 (bad range), 400 (malformed request), or 500 returns a plaintext status.
- No routing rules.
Mount with
app.use(...), notapp.get('/*splat', ...): the Express 5 wildcard doesn't match the root path/, so index resolution for/would be skipped.
TypeScript/ESM
npm install s3proxy express
npm install --save-dev @types/expressimport express, { type Request, type Response } from 'express';
import { S3Proxy, S3ProxyError } from 's3proxy';
import type { HttpRequest } from 's3proxy';
const app = express();
const proxy = new S3Proxy({ bucket: 'your-bucket-name' });
await proxy.init();
app.get('/*splat', async (req: Request, res: Response) => {
try {
const { stream, status, headers } = await proxy.fetch(req as unknown as HttpRequest);
res.writeHead(status, headers);
stream.on('error', () => res.end()).pipe(res);
} catch (err) {
const status = err instanceof S3ProxyError ? err.statusCode : 500;
res.status(status).end();
}
});
app.listen(3000);Now curl http://localhost:3000/index.html serves s3://your-bucket-name/index.html
Usage Examples
Express Integration with Error Handling
import express, { type Request, type Response } from 'express';
import { S3Proxy, S3ProxyError } from 's3proxy';
import type { HttpRequest } from 's3proxy';
const app = express();
const proxy = new S3Proxy({
bucket: 'my-website-bucket',
region: 'us-west-2',
});
try {
await proxy.init();
console.log('S3Proxy initialized successfully');
} catch (error) {
console.error('Failed to initialize S3Proxy:', error);
process.exit(1);
}
function handleError(req: Request, res: Response, err: unknown): void {
const statusCode = err instanceof S3ProxyError ? err.statusCode : 500;
const code = err instanceof Error ? err.name : 'InternalError';
const message = err instanceof Error ? err.message : String(err);
const errorXml = `<?xml version="1.0"?>
<error code="${code}" statusCode="${statusCode}" url="${req.url}">${message}</error>`;
if (res.headersSent) {
res.end();
return;
}
res.status(statusCode).type('application/xml').send(errorXml);
}
app.get('/*splat', async (req: Request, res: Response) => {
try {
const { stream, status, headers } = await proxy.fetch(req as unknown as HttpRequest);
res.writeHead(status, headers);
stream.on('error', (err) => handleError(req, res, err)).pipe(res);
} catch (err) {
handleError(req, res, err);
}
});
app.listen(3000);Fastify Integration
import Fastify from 'fastify';
import { S3Proxy, S3ProxyError } from 's3proxy';
import type { HttpRequest } from 's3proxy';
const fastify = Fastify({ logger: true });
const proxy = new S3Proxy({
bucket: 'my-website-bucket',
region: 'us-west-2',
});
await proxy.init();
fastify.get('/*', async (request, reply) => {
const { stream, status, headers } = await proxy.fetch(
request.raw as unknown as HttpRequest
);
reply.raw.writeHead(status, headers);
stream.on('error', () => reply.raw.end());
stream.pipe(reply.raw);
// Tell Fastify we've handled the response ourselves.
return reply.hijack();
});
// Fastify's setErrorHandler turns S3ProxyError instances into XML
// responses with the right status code (it honors err.statusCode).
fastify.setErrorHandler(async (error, request, reply) => {
const statusCode = error instanceof S3ProxyError ? error.statusCode : 500;
reply.status(statusCode).type('application/xml').send(`<?xml version="1.0"?>
<error code="${error.name}" statusCode="${statusCode}">${error.message}</error>`);
});
try {
await fastify.listen({ port: 3000 });
console.log('Server listening on http://localhost:3000');
} catch (err) {
fastify.log.error(err);
process.exit(1);
}Web Standard runtimes (Hono, Bun, Workers, Deno)
proxy.fetchWeb(request) is the Web-runtime convenience adapter over
fetch() (the counterpart of pipe() / middleware() for Node). It takes a
Web Request and returns a Web Response, so it is not tied to Hono - it
works with any framework exposing the raw Request, or as a runtime's
top-level fetch handler. With Hono it is one line:
import { Hono } from 'hono';
import { S3Proxy, S3ProxyError } from 's3proxy';
const proxy = new S3Proxy({ bucket: 'my-website-bucket' });
await proxy.init();
const app = new Hono();
app.get('/health', async (c) => {
await proxy.healthCheck();
return c.text('OK');
});
// One line: Web Request -> Web Response. fetchWeb throws the typed
// S3ProxyError on 404/403/416, which app.onError renders below.
app.on(['GET', 'HEAD'], '/*', (c) => proxy.fetchWeb(c.req.raw));
app.onError((err) => {
const status = err instanceof S3ProxyError ? err.statusCode : 500;
return new Response(err.message, { status });
});On other Web runtimes it is equally direct as the top-level handler:
// Bun / Deno / Cloudflare Workers
export default { fetch: (req: Request) => proxy.fetchWeb(req) };fetchWeb throws rather than rendering (it is the peer of fetch(), not
pipe()), so your framework's error handler owns the error-body format.
If you need to build the Response yourself (custom headers or error
body), call fetch() directly and adapt the stream:
import { Readable } from 'node:stream';
const { stream, status, headers } = await proxy.fetch({
url: new URL(c.req.url).pathname,
method: c.req.method,
headers: Object.fromEntries(c.req.raw.headers),
});
return new Response(Readable.toWeb(stream) as ReadableStream, { status, headers });Performance note. On Node, Hono goes through
@hono/node-serverwhich convertsIncomingMessageandWeb Request/Responseon every request, two extra layers compared with Express's directstream.pipe(res). For network-bound streaming (the typical s3proxy workload, where S3 round-trip time dominates) the conversion cost is negligible, and the smoke test's per-example latency baseline shows parity with Express and Fastify on small files. For CPU-bound paths measured in microseconds, prefer the Express or Fastify examples. On Bun, Workers, and Deno there is no conversion layer.
Framework Compatibility
s3proxy is framework-agnostic and works with any Node.js HTTP framework that provides access to the underlying request and response objects:
- Express - Fast, unopinionated web framework ✅
- Fastify - Fast and low overhead web framework ✅
- Hono - Web Standards framework (Bun/Workers/Deno/Node) ✅
- Koa - Expressive middleware framework ✅
- Hapi - Rich framework for building applications ✅
- NestJS - Progressive Node.js framework ✅
- Next.js API Routes - Full-stack React framework ✅
- Nuxt.js Server API - Vue.js framework ✅
- SvelteKit - Web development framework ✅
- Remix - Full stack web framework ✅
- AWS Lambda - Serverless functions ✅
- Vercel Functions - Edge and serverless functions ✅
- Netlify Functions - Serverless functions ✅
Key requirement: the framework must give you a request with url,
method, and headers (or path/query). All of the frameworks above
expose this, usually as req.raw, request.raw, or request.req.
Since proxy.fetch() returns { stream, status, headers } and never
touches a response, you wire the response however the framework prefers
(res.writeHead, a Web Response, reply.send, and so on).
Range Requests (Partial Content)
s3proxy handles HTTP Range requests for efficient streaming of large files:
# Download only bytes 0-99 of a large file
curl --range 0-99 http://localhost:3000/large-video.mp4 -o partial.mp4Large File Streaming
Stream large assets without server storage:
// Stream AI models, datasets, or media files
app.get('/models/:version/*splat', async (req: Request, res: Response) => {
try {
const { stream, status, headers } = await proxy.fetch(req as unknown as HttpRequest);
res.writeHead(status, headers);
stream.on('error', () => res.end()).pipe(res);
} catch (err) {
res.status((err as { statusCode?: number }).statusCode || 500).end();
}
});
// Now serve multi-GB files without local storage:
// GET /models/v1/llama-7b.bin streams from S3Health Checks
Built-in health check endpoint for load balancers:
app.get('/health', async (_req: Request, res: Response) => {
try {
await proxy.healthCheck(); // throws S3ProxyError on failure
res.status(200).type('text/plain').send('OK');
} catch (err) {
const status = err instanceof S3ProxyError ? err.statusCode : 503;
res.status(status).type('text/plain').send('unhealthy');
}
});Configuration
Constructor Options
import { S3Proxy } from 's3proxy';
import type { S3ProxyConfig } from 's3proxy';
const config: S3ProxyConfig = {
bucket: 'my-bucket', // Required: S3 bucket name
region: 'us-west-2', // Optional: AWS region
credentials: { // Optional: AWS credentials
accessKeyId: 'AKIA...',
secretAccessKey: '...'
},
endpoint: 'https://...', // Optional: Custom S3 endpoint
maxAttempts: 3, // Optional: Retry attempts
requestTimeout: 30000 // Optional: Request timeout in ms
};
const proxy = new S3Proxy(config);Environment Variables
BUCKET- S3 bucket namePORT- Server port (default: 3000)AWS_REGION- AWS regionNODE_ENV- Environment (enables credential file in dev mode)
API Reference
Class: S3Proxy
Constructor
new S3Proxy(config: S3ProxyConfig)Methods
await proxy.init(): Promise<void>
Initialize the underlying S3 client. By default also runs
healthCheck() against the bucket and rejects on failure (set
verifyOnInit: false to skip the probe; see
verifyOnInit).
await proxy.fetch(req: HttpRequest): Promise<S3FetchResponse>
Pure fetch. Returns { stream, status, headers } without touching a
response. Dispatches GET vs HEAD based on req.method (defaults to
GET). Throws a typed S3ProxyError on classified failures (404, 403,
416, malformed request); rethrows anything else. This is the primitive
the convenience adapters below are built on.
await proxy.pipe(req: HttpRequest, res: HttpResponse): Promise<void>
Convenience over fetch(): writes status + headers and pipes the body to
res. Resolves when the body finishes; writes the real 404/403/416 for a
classified S3 failure and does not reject for it. Rejects only if
res.writeHead throws. Use fetch() directly when you need a custom
error body.
proxy.middleware(): (req, res, next?) => void
Returns an Express/Connect handler built on pipe(). Drop it straight
into a route: app.get('/*splat', proxy.middleware()). Unexpected
(non-classified) errors are forwarded to next when present.
proxy.staticSite(options?: StaticSiteOptions): (req, res, next?) => void
Returns a handler that reproduces the core of S3 static website hosting
on top of fetch(): index-document resolution (/ to index.html,
/dir/ to dir/index.html) and, when errorDocument is set, serving it
with the original status for missing (404) and forbidden (403) keys.
Handles GET and HEAD and passes Range requests through. It does not
perform S3's trailing-slash directory redirect, and only 404/403 use the
error document. Mount with app.use(...) so the root path / is matched.
See Static website hosting.
await proxy.fetchWeb(request: Request): Promise<Response>
The Web-runtime adapter over fetch(): takes a Web Request, returns a Web
Response streaming the object. The Web analog of pipe() / middleware(),
for any WHATWG-fetch runtime (Bun, Deno, Cloudflare Workers, Node) and any
framework exposing the raw Request -
app.on(['GET','HEAD'], '/*', (c) => proxy.fetchWeb(c.req.raw)) or
export default { fetch: (req) => proxy.fetchWeb(req) }. Unlike pipe() it
does not render errors: it throws the typed S3ProxyError on 404/403/416,
so your framework's error handler owns the error-body format. HEAD responses
have a null body. Uses the runtime's global Request / Response; no extra
dependency.
await proxy.healthCheck(): Promise<void>
Verify bucket connectivity. Resolves on success, throws a typed
S3ProxyError on failure. Use it from a /health or /ready handler
and render the response yourself.
proxy.isInitialized(): void
Throws UserException if init() hasn't been called yet.
Static Methods
S3Proxy.version(): string
The library version.
Free functions exported from 's3proxy'
parseRequest(req), mapHeaderToParam(req, headerKey, paramKey), and
stripLeadingSlash(s) are exported as free functions. They previously
lived as S3Proxy.parseRequest and so on; the move makes them
tree-shakeable for callers that just want the parser.
Types
interface S3ProxyConfig extends S3ClientConfig {
bucket: string;
/**
* If true (default), init() calls healthCheck() and rejects when
* the bucket is unreachable, to fail fast on misconfiguration.
* Set to false in orchestrator deployments (Kubernetes, ECS).
*/
verifyOnInit?: boolean;
}
/** What proxy.fetch() returns. */
interface S3FetchResponse {
stream: NodeJS.ReadableStream;
status: number;
headers: Record<string, string>;
}
/**
* Structural subset of an HTTP request that proxy.fetch() reads.
* Express's Request, Fastify's request.raw, and Node's IncomingMessage
* all satisfy it (with light casts).
*/
interface HttpRequest {
url: string;
method?: string;
headers: Record<string, string | string[]>;
path?: string;
query?: Record<string, string | string[]>;
}
interface ParsedRequest {
key: string;
query: Record<string, string | string[]>;
}
/**
* Structural response the convenience adapter (pipe/middleware/staticSite)
* writes to. Node's ServerResponse, Express's Response, and Fastify's
* reply.raw all satisfy it. The pure fetch() primitive never touches it.
*/
interface HttpResponse extends NodeJS.WritableStream {
writeHead(statusCode: number, headers?: Record<string, string>): unknown;
readonly headersSent?: boolean;
}
/** Options for proxy.staticSite(). */
interface StaticSiteOptions {
indexDocument?: string; // default 'index.html'; '' disables
errorDocument?: string; // optional; served with the original 4xx status
}Error Handling
import {
S3ProxyError, // base class (statusCode, code, cause)
S3NotFound, // 404: NoSuchKey or NoSuchBucket
S3Forbidden, // 403: AccessDenied
S3InvalidRange, // 416: InvalidRange
// InvalidRequest: 400, thrown by parseRequest for malformed input
} from 's3proxy';
try {
const { stream, status, headers } = await proxy.fetch(req);
res.writeHead(status, headers);
stream.pipe(res);
} catch (err) {
if (err instanceof S3NotFound) return send404(res);
if (err instanceof S3Forbidden) return send403(res);
if (err instanceof S3InvalidRange) return send416(res);
if (err instanceof S3ProxyError) return res.status(err.statusCode).end();
throw err; // unknown: let the framework's error handler take it
}The proxy is also an EventEmitter. It emits init when init()
resolves and error when it rejects, which is useful for logging
dashboards.
proxy.on('error', (err: Error) => console.error('S3Proxy error:', err));
proxy.on('init', () => console.log('S3Proxy initialized'));verifyOnInit for orchestrators
In Kubernetes/ECS deployments, you typically want the platform's own
readiness probe to determine health rather than crash-looping the pod
when S3 has a hiccup at boot. Pass verifyOnInit: false and call
proxy.healthCheck() from your readiness handler instead.
const proxy = new S3Proxy({ bucket: 'my-bucket', verifyOnInit: false });
await proxy.init(); // resolves immediately, no S3 traffic
app.get('/ready', async (_req, res) => {
try {
await proxy.healthCheck();
res.status(200).send('ready');
} catch (err) {
res.status(503).send(String(err));
}
});Docker Deployment
For containerized deployments:
docker run --env BUCKET=mybucket --env PORT=8080 --publish 8080:8080 -t forkzero/s3proxy:latestThe forkzero/s3proxy image is published from
forkzero/s3proxy-docker with the
tags X.Y.Z, X.Y, and latest. latest is shown here for quick start; pin a
specific version for production (for example forkzero/s3proxy:4.2).
For local development with temporary AWS credentials, write a session
token to ~/.s3proxy/credentials.json and bind-mount it into the
container. Keeping credentials in your home directory (not the repo
root) avoids any chance of committing them:
mkdir -p ~/.s3proxy
aws sts get-session-token --duration 900 > ~/.s3proxy/credentials.json
docker run \
-v ~/.s3proxy/credentials.json:/src/credentials.json:ro \
-e BUCKET=mybucket \
-e PORT=8080 \
-e NODE_ENV=dev \
-p 8080:8080 \
-t forkzero/s3proxy:latestDevelopment
TypeScript Development
# Install dependencies
npm install
# Development with hot reload
npm run dev
# Build TypeScript
npm run build
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Type checking
npm run type-checkTesting & Quality Assurance
Tests run across several dimensions:
Test Coverage Matrix
| Test Type | Local (Makefile) | CI (GitHub Actions) | Description |
|---------------|----------------------|-------------------------|-----------------|
| Code Quality | | | |
| Lint | make lint | ✅ Node CI | Code style and quality checks |
| Type Check | make type-check | ✅ Node CI | TypeScript type safety validation |
| Security Audit | npm audit | ✅ Node CI | Dependency vulnerability scanning |
| Unit Testing | | | |
| Unit Tests | make unit-tests | ✅ Node CI | Core functionality testing |
| Coverage | npm run test:coverage | ✅ Node CI | Code coverage reporting (96%+) |
| Integration Testing | | | |
| Build Verification | make build | ✅ Node CI | TypeScript compilation |
| Package Verification | make pre-release-check | ✅ Node CI | npm package integrity |
| Functional Testing | | | |
| Validation Tests | make test-validation-docker | ✅ Node CI | 24 comprehensive functionality tests |
| Binary Integrity | Included in validation | ✅ Node CI | File corruption detection |
| Range Requests | Included in validation | ✅ Node CI | HTTP range request handling |
| Error Handling | Included in validation | ✅ Node CI | Proper error status codes |
| Performance Testing | | | |
| Load Testing | make artillery-docker | ✅ Node CI | High-throughput performance |
| Stress Testing | make test-performance | ✅ Node CI | Resource usage under load |
| Platform Testing | | | |
| Docker Integration | make test-all-docker | ✅ Node CI | Containerized deployment |
| Multi-Node | Node 22, 23 | ✅ Node CI | Cross-version compatibility |
Test Commands
# Run all tests locally
make all # Complete test suite
make test # Core tests (build, lint, unit)
make functional-tests # Integration and Docker tests
# Individual test categories
make test-validation-docker # 24 comprehensive validation tests
make artillery-docker # Performance/load testing
# Quality checks
make pre-release-check # Complete pre-release verificationContinuous Integration
- Every Push: Core tests (lint, type-check, build, unit tests)
- Master Branch: Full test suite including validation and performance
- Pull Requests: Complete verification before merge
- Releases: Comprehensive pre-release checks
Project Structure
src/
├── index.ts # S3Proxy orchestrator
├── request-parser.ts # parseRequest, mapHeaderToParam, etc.
├── s3-gateway.ts # AWS SDK boundary; turns SDK output into S3FetchResponse
├── errors.ts # S3ProxyError + S3NotFound / S3Forbidden / S3InvalidRange / InvalidRequest
├── UserException.ts # Caller-misuse errors (uninitialized, missing bucket)
├── types.ts # Public type definitions
└── version.ts # Version (read from package.json at module load)
examples/
├── express-basic.ts # TypeScript Express example
├── fastify-basic.ts # TypeScript Fastify example
├── fastify-docker.ts # Dockerized Fastify example
├── hono-basic.ts # Hono / Web Standards example
├── static-site.ts # staticSite index + error document example
└── http.ts # TypeScript node:http example
test/
├── s3proxy.test.ts # Constructor / init / healthCheck / verifyOnInit
├── fetch.test.ts # proxy.fetch() behavioral coverage
├── pipe.test.ts # proxy.pipe() and proxy.middleware()
├── static-site.test.ts # proxy.staticSite() behavior
├── parse-request.test.ts # parseRequest unit tests (incl. malformed encoding)
├── mock-express.test.ts # Express request-shape compatibility
├── mock-fastify.test.ts # Fastify request-shape compatibility
├── streaming-memory.test.ts # 100MB synthetic body, peak RSS bound
├── concurrent.test.ts # 10 parallel fetch() calls
├── version.test.ts # Version tests
├── imports-esm.test.ts # ESM import tests
├── package-exports.test.ts # Package export tests
├── helpers/
│ └── aws-mock.ts # AWS SDK mocking utilities
└── integration/
└── validation.test.js # End-to-end validation testsConfiguration Files
s3proxy uses several configuration files for different aspects of development and deployment:
TypeScript Configuration
tsconfig.json- Main TypeScript compiler configuration- Compiles
src/todist/src/for npm package - ES2022 target with NodeNext module resolution
- Strict type checking enabled
- Compiles
tsconfig.examples.json- Type checking for examples- Extends main config with examples-specific settings
- Used by
npm run type-checkto validate examples - Ensures examples stay current with API changes
Testing & Quality
vitest.config.ts- Unit test configuration- Unit test settings with 30s timeout
- Coverage reporting (text, HTML, LCOV, JSON)
- Coverage thresholds: branches 85%, functions 95%, lines/statements 90%
- Excludes integration tests and examples from unit test runs
vitest.integration.config.ts- Integration test configuration- Runs validation tests that require a live server
- Used by
npm run test:validationand Makefile targets - Separate from unit tests for faster development workflow
biome.json- Code formatting and linting- Fast alternative to ESLint + Prettier
- Consistent code style across the project
- Import organization and formatting rules
Release & CI/CD
.releaserc.json- Semantic release configuration- Conventional commits for automated versioning
- Generates CHANGELOG.md automatically
- Publishes to npm and creates GitHub releases
- Handles version bumping and git tagging
GitHub Actions
.github/workflows/nodejs.yml- Main CI pipeline- Core tests (lint, type-check, build, unit tests)
- Validation tests (24 comprehensive functionality tests)
- Performance testing with Artillery
- Package verification
.github/workflows/release.yml- Automated releases.github/workflows/manual-release.yml- Manual release workflow
Performance Testing
Artillery load-test configs and scenarios come from
@forkzero/s3-website-test-kit,
a devDependency shared with forkzero/s3proxy-docker. make artillery-docker
runs the kit's configs/load-test.yml + scenarios/core/load-test.yml against
the container. The kit is target-agnostic (native S3 website hosting or
s3proxy); its scenarios/core/ are portable, and scenarios/s3proxy/ holds the
s3proxy-only checks (for example /health).
range-requests.yml- HTTP range request testing
Development Tools
.vscode/settings.json- VS Code workspace settings- Disables automatic Makefile configuration prompts
.github/dependabot.yml- Automated dependency updatesMakefile- Build automation and testing orchestration- Coordinates Docker and Artillery testing
- Provides consistent commands across environments
AWS & Docker
examples/aws-ecs/- ECS deployment configurations- CloudFormation templates for production deployment
Use Cases
- Static websites - Serve React/Vue/Angular builds from S3
- File downloads - Stream large files without server storage
- Media serving - Video/audio streaming with range request support
- API backends - Serve user uploads or generated content
- AI & ML workflows - Stream models, datasets, and training data
- CDN alternative - Cost-effective content delivery
Performance
See docs/performance.md for detailed performance testing and benchmarks.
Getting Help
- Maintenance Guide: for contributors and advanced usage
- Report issues
- Discussions
Contributing
Contributions are welcome. See the Maintenance Guide for development setup and contribution guidelines.
License
Apache 2.0 - see LICENSE file.
