BentoS3 : a lightweight S3-compatible server for local development and tests

How to use BentoS3 as a local S3-compatible server for development, CI, and integration tests.

  ·   6 min read

I recently published the first stable version of BentoS3, a lightweight S3-compatible server for local development, automated tests, and CI environments.

The project came from a practical need. In our local environment, we were using MinIO as an S3 replacement. It worked, but we were still pinned to an old version, before the project direction changed and several features were removed from the free version UI and now the project is no longer maintained. That made the local setup feel more fragile than it needed to be.

At the same time, our integration tests depended on containers just to exercise code paths that talked to S3. That added setup cost, made the test environment heavier, and slowed down feedback loops. For the kind of S3 usage we had, we did not need a full object storage server. We needed something small, embeddable, and predictable.

That is the goal of BentoS3: provide a practical subset of the S3 API that is easy to run locally and easy to embed in tests.

It is not production object storage. It does not try to implement every S3 feature. The supported APIs are mostly based on the usage I had with S3, but I am open to improving compatibility over time based on real use cases.


What BentoS3 Provides #

BentoS3 can be used in two main ways:

  • As a standalone S3-compatible server started from the CLI.
  • As an embeddable Node.js library for Vitest, Jest, or framework-based integration tests.

It works with the official AWS SDK for JavaScript v3 using path-style addressing, and stores data on the local filesystem by default.

The main use cases are:

  • Local development when your application expects an S3 endpoint.
  • Integration tests that need bucket and object operations.
  • CI jobs where you want to avoid Docker or external services.
  • Framework apps that need a local S3-compatible route during development or testing.

Installation #

Install it from npm:

npm install bento-s3

BentoS3 requires Node.js 20 or newer.


Running BentoS3 Locally #

The fastest way to start a local S3-compatible server is with the CLI:

npx bentos3 serve

By default, the server listens on 127.0.0.1:9000 and stores its data in ./.bentos3.

You can also configure the host, port, and storage directory explicitly:

npx bentos3 serve --host 127.0.0.1 --port 9000 --root-dir ./.bentos3

On first start, BentoS3 bootstraps a default access key and prints the credentials. It also includes a small server-rendered dashboard.

You can create a dashboard user with:

npx bentos3 user create admin

Then open the dashboard at:

http://127.0.0.1:9000/ui

The dashboard lets you browse buckets, upload and download objects, and manage access keys.


Connecting With The AWS SDK #

BentoS3 targets the official AWS SDK for JavaScript v3. The important part is to configure a custom endpoint and enable path-style addressing.

import { CreateBucketCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";

const client = new S3Client({
  region: "us-east-1",
  endpoint: "http://127.0.0.1:9000",
  forcePathStyle: true,
  credentials: {
    accessKeyId: "bentos3",
    secretAccessKey: process.env.BENTOS3_SECRET_ACCESS_KEY ?? "replace-me",
  },
});

await client.send(new CreateBucketCommand({ Bucket: "example-bucket" }));

await client.send(
  new PutObjectCommand({
    Bucket: "example-bucket"Proceed,
    Key: "hello.txt",
    Body: "Hello BentoS3",
  }),
);

If your application already uses the AWS SDK, this usually means the only test/local-specific configuration you need is the endpoint, credentials, and forcePathStyle: true.


Using BentoS3 In Integration Tests #

The most useful part for me is the managed test server. Instead of starting a container before the test suite, you can start BentoS3 directly from Node.js.

Here is a minimal Vitest-style setup:

import { CreateBucketCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { afterAll, beforeAll, expect, test } from "vitest";
import { BentoS3, MemoryAuthStore } from "bento-s3";

let server: BentoS3;
let client: S3Client;

beforeAll(async () => {
  const authStore = new MemoryAuthStore();

  await authStore.createCredential({
    accessKeyId: "test",
    secretAccessKey: "test-secret",
  });

  server = new BentoS3({
    port: 0,
    authStore,
    dashboard: { enabled: false },
  });

  await server.start();

  client = new S3Client({
    region: "us-east-1",
    endpoint: server.endpoint,
    forcePathStyle: true,
    credentials: {
      accessKeyId: "test",
      secretAccessKey: "test-secret",
    },
  });
});

afterAll(async () => {
  await server.stop();
});

test("uploads an object", async () => {
  await client.send(new CreateBucketCommand({ Bucket: "photos" }));

  await client.send(
    new PutObjectCommand({
      Bucket: "photos",
      Key: "cat.jpg",
      Body: Buffer.from("image-bytes"),
    }),
  );

  expect(true).toBe(true);
});

Using port: 0 asks Node.js to allocate an available port. BentoS3 exposes the final URL through server.endpoint, so tests can run without hard-coded ports and without conflicts between parallel jobs.

For tests, MemoryAuthStore keeps credentials in memory. For local development, BentoS3 also provides a JSON-backed auth store that persists credentials on disk.


Embedding BentoS3 In A Framework #

BentoS3 is built around a framework-neutral core. Adapters translate framework requests into BentoS3’s internal request format.

For example, with Express:

import express from "express";
import { MemoryAuthStore } from "bento-s3";
import { BentoS3Core } from "bento-s3/core";
import { expressAdapter } from "bento-s3/adapters/express";

const app = express();
const authStore = new MemoryAuthStore();

await authStore.createCredential({
  accessKeyId: "test",
  secretAccessKey: "test-secret",
});

const bento = new BentoS3Core({ authStore });

app.use("/s3", expressAdapter(bento));

When mounted under /s3, the AWS SDK endpoint should include that path:

const client = new S3Client({
  endpoint: "http://127.0.0.1:3000/s3",
  forcePathStyle: true,
  region: "us-east-1",
  credentials: {
    accessKeyId: "test",
    secretAccessKey: "test-secret",
  },
});

There are also adapters for Koa, Fastify, Node HTTP, and Fetch-style handlers. One important detail when embedding BentoS3 in a framework is body parsing: BentoS3 must receive the raw request stream, so mount the S3 route before JSON/body parsers for that route.


Supported S3 Operations #

BentoS3 1.0 supports the operations I needed most often in local development and integration tests:

  • ListBuckets
  • CreateBucket
  • DeleteBucket
  • HeadBucket
  • ListObjectsV2
  • PutObject
  • GetObject
  • HeadObject
  • DeleteObject
  • DeleteObjects
  • CopyObject

This is enough for a lot of common application-level S3 usage: create a bucket, upload files, read them back, list objects, copy objects, and clean up after tests.


Current Limitations #

BentoS3 intentionally starts with a practical subset instead of trying to clone all of S3.

The main limitations today are:

  • Requests must use path-style addressing.
  • SigV4 header authentication is supported, but presigned URL query authentication is not supported yet.
  • ListObjectsV2 supports prefix filtering, but not delimiter grouping, continuation tokens, StartAfter, or custom MaxKeys pagination.
  • Multipart upload, range requests, ACLs, bucket policies, object tagging, lifecycle policies, replication, and object lock are not supported.
  • Bucket names are restricted to filesystem-safe path segments.

If you need one of these features for a real local development or testing use case, opening an issue is useful. I would rather improve parity based on concrete usage than implement a large surface area blindly.


Conclusion #

BentoS3 is a small tool for a specific job: make S3-dependent local development and integration tests easier to run.

For our use case, replacing a heavier local object storage setup and removing containers from the test path made the environment simpler and faster. Version 1.0.0 is the first stable release of that idea.

You can find the package, full README, examples, and compatibility notes on GitHub:

https://github.com/gtindo/BentoS3