npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

s3proxy

v4.2.1

Published

Streaming web proxy for AWS S3

Readme

s3proxy

NPM Version NPM Downloads Node CI Docker Pulls

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 express
import 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 /*splat wildcard is Express 5's path-to-regexp requirement; 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. staticSite does not issue S3's /photos to /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(...), not app.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/express
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: '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-server which converts IncomingMessage and Web Request/Response on every request, two extra layers compared with Express's direct stream.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:

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.mp4

Large 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 S3

Health 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 name
  • PORT - Server port (default: 3000)
  • AWS_REGION - AWS region
  • NODE_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:latest

The 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:latest

Development

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-check

Testing & 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 verification

Continuous 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 tests

Configuration Files

s3proxy uses several configuration files for different aspects of development and deployment:

TypeScript Configuration

  • tsconfig.json - Main TypeScript compiler configuration
    • Compiles src/ to dist/src/ for npm package
    • ES2022 target with NodeNext module resolution
    • Strict type checking enabled
  • tsconfig.examples.json - Type checking for examples
    • Extends main config with examples-specific settings
    • Used by npm run type-check to 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:validation and 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 updates
  • Makefile - 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

Contributing

Contributions are welcome. See the Maintenance Guide for development setup and contribution guidelines.

License

Apache 2.0 - see LICENSE file.