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

bakeway

v0.1.8

Published

A TypeScript web framework for Bun

Readme

Bakeway

A TypeScript web framework for Bun with no external runtime libraries. Bakeway handles ordinary Request objects and returns Response objects; your application owns its server, startup, shutdown, WebSockets, authentication and services.

The npm package is bakeway; its source repository directory is Bakeway. Version 0.1.8 targets Bun 1.4.2 and TypeScript 7.0.2 with strict ESM/bundler resolution. Compiled ESM and complete declarations are included. Import from the package root.

bun add [email protected]

Existing users of wizepal-server can install bakeway and change their imports to "bakeway". The exported API is unchanged.

Version 0.1.8

This patch fixes Redis credential URL encoding, preserving literal % characters and percent-looking text in raw usernames/passwords. It adds a 169-case Bun unit suite, strict compile-only contract checks, and test gates before release packaging. Native Redis protocol checks verify credential round-tripping in the installed package; a separate opt-in integration test supports checking your own Redis connection.

Public APIs and runtime dependencies are unchanged. See Unit tests for commands and the documented string-date coverage limitation.

Application and controllers

import { Application, Controller, Service, Lib, type Routes, type ApplicationOptions } from "bakeway";

class GreetingService extends Service {
	greet() {
		return "Hello";
	}
}
class LocalLib extends Lib {
	static timestamp() {
		return this.Date.Now().toISOString();
	}
}
class APIController extends Controller {
	private service = new GreetingService();

	GET() {
		this.get.set("hello", this.hello.bind(this));
		this.get.set("text", () => new Response("Hello", { status: 200 }));
	}
	POST() {}

	private hello(request: Request) {
		const name = Controller.GetQueryParams(request).get("name");
		return Controller.Response({ message: this.service.greet(), name, at: LocalLib.timestamp() }, true, 201);
	}
}
const routes: Routes = { api: APIController };
const options: ApplicationOptions = {
	surfaces: [
		{
			async handle(request) {
				return new URL(request.url).pathname === "/health" ? new Response("ok") : null;
			},
		},
	],
	middlewares: [
		{
			async handle(request, next) {
				const response = await next();
				response.headers.set("x-example", "yes");
				return response;
			},
		},
	],
};
const application = new Application(routes, options);
const response = await application.handle(new Request("https://example.test/api/hello?name=Ada"));

Surfaces run first in configured order. A handled surface bypasses middleware and routing, so each surface owns any required authentication. On a miss, middleware wraps dispatch, first middleware outermost; next() may run only once. Result conversion finishes before middleware resumes.

Routes and option lists are snapshotted at construction. Each Application lazily constructs and reuses its own controllers. Registration runs synchronously after the concrete constructor and field initializers finish. Failed construction/registration is not cached. Controllers are shared across requests within an Application: keep request-specific data in local variables.

GET/POST maps use the path after the controller prefix as the action name (including any remaining slashes). Missing controllers/actions return 404; a known action with an unsupported method returns 405 with Allow. Other HTTP methods can be handled by surfaces.

Controller.Response(data, status = true, http_status?) creates an envelope. status accepts a boolean or number and is distinct from the HTTP status. Application serializes the entire envelope, including http_status when provided, using HTTP http_status ?? 200. Native Responses pass through unchanged. Subclasses may override protected toResponse(result, request) and onError(error, request) with synchronous or asynchronous implementations. Errors in awaited surfaces, middleware, dispatch and conversion reach onError. A failing error hook gets a direct generic 500 fallback.

Controller factories

Routes accept either a zero-argument controller constructor or a synchronous factory returning a Controller subclass:

import { Application, Controller, type Routes } from "bakeway";

class Sessions {
	readonly active = 0;
}
class GatewayController extends Controller {
	constructor(private readonly sessions: Sessions) {
		super();
	}
	GET() {
		this.get.set("count", () => Controller.Response(this.sessions.active, true));
	}
	POST() {}
}
class HealthController extends Controller {
	GET() {
		this.get.set("check", () => new Response("ok"));
	}
	POST() {}
}
const sessions = new Sessions();
const routes = {
	gateway: { create: () => new GatewayController(sessions) },
	health: HealthController,
} satisfies Routes;
const application = new Application(routes);

ControllerConstructor, ControllerFactory, RouteDefinition and Routes are exported types. Async factories and non-controller results are rejected by TypeScript. Each Router snapshots route entries and factory function references, creates controllers lazily on the first request for that route, runs POST/GET registration after construction, and caches only successfully initialized controllers per route. Construction, factory and registration failures propagate through the existing error boundary and may be retried on subsequent requests. Each Router/Application owns its cache; return a new controller from each factory invocation to preserve controller isolation. Dependencies captured by factories remain application-owned and are never disposed of or managed by the framework.

Low-level Router

import { Router } from "bakeway";

const router = new Router(routes);
const result = await router.call(new Request("https://example.test/api/hello"));
const query = Router.GetQueryParams(new Request("https://example.test/?key=value"));

Each Router owns a route snapshot and independent controller cache. It returns the raw controller envelope or Response and propagates errors. Application supplies the surfaces, middleware, conversion and HTTP error boundary. Router has no singleton, route replacement API or Router injection option.

Service helpers and utilities

Service.validateRequestBody<T>(request, required_keys) reads, parses and checks required keys. An instance convenience method delegates to it. Service.parseJsonRequestBody(rawBody) first tries JSON, then retains the permissive normalization fallback (smart quotes, comments, newlines and whitespace). Service.validateRequestBodyObject<T> checks nil and key presence; these checks are not value/schema validation.

Service.validateSignedRequestBody<T>(request, required_keys, verifier) passes the original raw text and Headers to your verifier before parsing, and returns { body, rawBody }. Verifier/read failures propagate unchanged. Expected parser/body failures use framework OpsError codes.

Service.ExtractApiKey(request, body?) selects a nonblank exact Bearer header value, then a supplied body.api_key, then the URL query parameter api_key (for example, ?api_key=your-key). Header/body checks retain their existing Guards classification. Query values are URL-decoded and trimmed; blank values are ignored, and the first value is used when the parameter is repeated. It does not read the body or validate credentials. Missing input throws service:missing-api-key (default HTTP 400).

Supplied Guards, Lib, E_IS, NonNullableType, JsonValue and JsonObject are exported unchanged. In particular, Guards classifies numeric strings as numbers and IsType checks presence of keys rather than their value types.

Singleton utility

import { Singleton } from "bakeway";

class Settings extends Singleton {
	private constructor(public readonly region: string) {
		super();
	}

	static Open(region: string): Settings {
		return this.GetInstance(region);
	}
}

const settings = Settings.Open("eu");
Settings.Open("us") === settings; // true; region remains "eu"
Settings.RemoveInstance();
const replacement = Settings.Open("us"); // a new instance

The existing droplet-bot implementation is exported as Singleton, with Constructor and ISingletonConstructor types. Each concrete subclass has its own cached instance. GetInstance() supports no argument or one argument; only the first successful construction uses that argument. RemoveInstance() clears that subclass's cache without disposing the old object. Failed construction is not cached. Argument types retain the original permissive API; use a typed static wrapper such as Open to enforce your constructor's requirements.

Databases and Redis

Database extends Singleton: each concrete database subclass has one cached instance. Connection(...args) infers the public constructor's argument tuple and returns that subclass. A no-argument constructor needs no arguments; a configured constructor requires its arguments on every call.

import { Database, RedisDatabase, type RedisOptions } from "bakeway";

class LocalDatabase extends Database {
	protected async open() {
		/* establish connection */
	}
	protected async close() {
		/* release connection */
	}
}
const local = await LocalDatabase.Connection();

const options = {
	host: "localhost",
	port: 6379,
	// username: "default",
	// password: "...",
	// tls: true,
} satisfies RedisOptions;

const redis = await RedisDatabase.Connection(options);
await redis.client.set("greeting", "hello");
const greeting = await redis.client.get("greeting"); // string | null
await redis.disconnect();

The base owns connect() and disconnect() and their connected/processing state; subclasses implement protected open() and close() hooks. Concurrent connection calls share one attempt and all await readiness. Failure clears the pending attempt so a later call can retry. Disconnect waits for an active connection attempt to settle. Reconnecting reuses the same database instance. Use Connection() to initialize database singletons, rather than the inherited raw GetInstance().

Configuration is fixed for the lifetime of a cached instance. Redis snapshots its connection URL and accepts equivalent options on subsequent calls; changed options throw without reporting credentials. Generic databases compare constructor arguments using Object.is (object identity); reuse those objects without mutating them, or override matchesConnectionArguments(args, initial) for custom comparison. Disconnect before calling inherited RemoveInstance() to reset configuration; removal alone does not close resources.

Redis requires host and numeric port; username/password are optional and safely URL-encoded, and tls defaults to false (true uses rediss). Environment-variable reading stays in the application. client exposes Bun's native RedisClient and becomes available during connection; await Connection() or connect() before issuing commands. The adapter awaits the native connection handshake, closes failed clients, and reports the client's current connection status. It does not log credentials or read application environment settings.

The package includes @types/bun for fully typed native APIs. These are declaration dependencies; Redis itself is provided by Bun, with no external runtime client library.

Typed local errors

import { OpsError, Application, type OpsErrorContext, type ErrorCode, type ErrorDomain, type ErrorType, type ErrorArguments } from "bakeway";

const gatewayErrors = {
	"gateway:session-not-found": (context: OpsErrorContext & { sessionId: string }) => `Session ${context.sessionId} was not found.`,
};
const GatewayError = OpsError.extend(gatewayErrors);

type GatewayCode = ErrorCode<typeof gatewayErrors>;
type GatewayDomain = ErrorDomain<typeof gatewayErrors>;
type GatewayErrorType = ErrorType<"gateway", typeof gatewayErrors>;
type AllErrorTypes = ErrorType<undefined, typeof gatewayErrors>;
type SessionArguments = ErrorArguments<"gateway:session-not-found", typeof gatewayErrors>;

class GatewayApplication extends Application {
	protected override onError(error: unknown, request: Request) {
		if (error instanceof GatewayError && error.code === "gateway:session-not-found") {
			return Response.json({ message: "Session not found" }, { status: 404 });
		}
		return super.onError(error, request);
	}
}
throw new GatewayError("gateway:session-not-found", { sessionId: "example" });

Use the const form when defining only a catalog. Its constructor infers the code from the first argument, so a required context remains directly accessible:

const error = new GatewayError("gateway:session-not-found", { sessionId: "example" });
error.context.sessionId; // string

Use a subclass when adding behavior. An ordinary subclass has one instance type; use is(code) to narrow both its code and context, including after instanceof in an error handler:

class LocalError extends OpsError.extend(gatewayErrors) {
	#origin = "gateway";
	origin() {
		return this.#origin;
	}
}

const error = new LocalError("gateway:session-not-found", { sessionId: "example" });
if (error.is("gateway:session-not-found")) {
	error.context.sessionId; // string
}

function handle(error: unknown) {
	if (error instanceof LocalError && error.is("gateway:session-not-found")) {
		return error.context.sessionId;
	}
}

is(code) is available on both forms and compares the instance's code. LocalError.extend(...) uses real superclass construction: LocalError's constructor, field initializers and private fields are initialized for derived instances. The most-derived catalog supplies the message while inherited codes remain supported.

Message-function arguments determine required, optional or absent context. Unknown codes and missing required context are compiler errors. GetMessage uses the class's own catalog. Instances retain readonly code and typed context, standard cause, and instanceof OpsError identity. Catalogs are snapshotted and independent; inherited codes cannot be replaced. Helpers take the additional definitions object type, include framework definitions, and default to framework-only meanings when additions are omitted.

Only known HTTP framework codes receive default mappings. Expected Service failures map to 400, missing routes/actions to 404, and method mismatch to 405. Other exceptions, configuration-loader errors and consumer errors receive the fixed application:internal-error 500 envelope unless explicitly mapped by your Application. Diagnostic messages, causes and context are never copied into default responses.

Unit tests

bun run test                      # Isolated Bun tests and strict compiler fixtures
bun run test:unit                 # Runtime tests only
bun run typecheck:tests           # Runtime/helper types and compile-only contracts
TZ=UTC bun test --isolate ./tests/unit/database
bun run test:unit --randomize     # Verify order independence
bun run test:unit --coverage      # Informational coverage; no percentage gate

Tests live under tests/unit/<subject>/. They exercise in-memory HTTP behavior, service parsing/authentication, errors, singleton/database lifecycles, utilities and public exports. Redis unit tests use a controlled internal factory and open no sockets. The canonical runtime command uses --isolate to prevent module mocks leaking across files; use it instead of bare bun test for the full suite. Adjacent *.types.ts files compile strictly and never execute. Neither tests nor their helpers/configuration enter the package archive.

RedisOptions.username and password accept raw credentials. Do not URL-encode them before passing them; Bakeway encodes them when constructing the connection URL, including literal % and %2F text.

The existing string-input branch of Lib.Date.AddTimeFromDate ignores the offset. That behavior remains an explicitly disclosed gap pending a separate decision; tests cover its Date-input branch without treating the string behavior as correct.

Redis connection integration test

The unit suite uses fake Redis clients and requires no credentials. To explicitly check a real Redis endpoint, set REDIS_HOST, REDIS_PASSWORD, REDIS_PORT, and REDIS_USERNAME in your ignored local .env, then run:

bun run test:integration:redis

This separate Bun integration test loads .env, connects through RedisDatabase, sends PING, and disconnects. It does not read, write, or delete application keys. TLS is enabled by default; set REDIS_TLS=false only for a server intentionally configured without TLS. There is no automatic plaintext fallback. Native error details are suppressed to keep credentials out of test output.

The command explicitly sets BAKEWAY_REDIS_INTEGRATION=1. Without that opt-in, the integration case is skipped even if discovered by bare bun test. It is excluded from the regular bun run test / test:unit paths and is not a release prerequisite. Passing this check proves connectivity for the supplied configuration, not arbitrary credential encoding, failure recovery, or application integration.

Development and release

bun install
bun run test
bun run typecheck
bun run build
bun run release --dry-run
bun run release --publish /absolute/path/to/bakeway-0.1.8.tgz

Dry-run requires the isolated unit suite and strict test compiler to pass before building or packing. It then checks archive contents, installed declarations and runtime in a disposable consumer, including native Redis authentication with synthetic percent-containing credentials against a local protocol fixture. The optional live Redis check is separate. Publication rechecks that exact archive and its recorded integrity, uploads it without rebuilding, then compares registry integrity and checks an unauthenticated exact-version installation. The scripts do not change versions or run Git commands. They target public [email protected] on registry.npmjs.org with the normal latest tag.

For publication, supply NPM_TOKEN in the caller environment or ignored local .env (see .env.example). The caller environment takes precedence. Only that key is read; dotenv content is not executed. Authentication uses a registry-scoped temporary npm configuration removed on exit. Never commit credentials. Internal design documents and authentication files are excluded from the package.

License: UNLICENSED. Public npm availability does not grant an open-source license.