diff --git a/packages/api-mux/README.md b/packages/api-mux/README.md
new file mode 100644
index 0000000..5ef5854
--- /dev/null
+++ b/packages/api-mux/README.md
@@ -0,0 +1,58 @@
+# `@lowerdeck/api-mux`
+
+Route multiplexer for API endpoints. Dispatches requests to different services based on domain, path, and HTTP method with support for fallback handlers.
+
+## Installation
+
+```bash
+npm install @lowerdeck/api-mux
+yarn add @lowerdeck/api-mux
+bun add @lowerdeck/api-mux
+pnpm add @lowerdeck/api-mux
+```
+
+## Usage
+
+```typescript
+import { apiMux } from '@lowerdeck/api-mux';
+
+// Define your services
+const mux = apiMux([
+ {
+ domains: ['api.example.com'],
+ methods: ['GET', 'POST'],
+ endpoint: {
+ path: '/users',
+ fetch: async (req) => {
+ return new Response('Users API');
+ }
+ }
+ },
+ {
+ methods: ['POST'],
+ endpoint: {
+ path: '/webhooks',
+ exact: true, // Only match exact path
+ fetch: async (req) => {
+ return new Response('Webhook handler');
+ }
+ }
+ }
+], async (req, server) => {
+ // Fallback handler for unmatched routes
+ return new Response('Not found', { status: 404 });
+});
+
+// Use with your HTTP server
+const server = Bun.serve({
+ fetch: mux
+});
+```
+
+## License
+
+This project is licensed under the Apache License 2.0.
+
+
diff --git a/packages/api-mux/package.json b/packages/api-mux/package.json
new file mode 100644
index 0000000..3cd9e09
--- /dev/null
+++ b/packages/api-mux/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@lowerdeck/api-mux",
+ "version": "1.0.0",
+ "publishConfig": {
+ "access": "public"
+ },
+ "author": "Tobias Herber",
+ "license": "Apache 2",
+ "type": "module",
+ "source": "src/index.ts",
+ "exports": {
+ "types": "./dist/index.d.ts",
+ "require": "./dist/index.cjs",
+ "import": "./dist/index.module.js",
+ "default": "./dist/index.module.js"
+ },
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.module.js",
+ "types": "dist/index.d.ts",
+ "unpkg": "./dist/index.umd.js",
+ "scripts": {
+ "test": "vitest run --passWithNoTests",
+ "lint": "prettier src/**/*.ts --check",
+ "build": "microbundle"
+ },
+ "devDependencies": {
+ "microbundle": "^0.15.1",
+ "@lowerdeck/tsconfig": "^1.0.0",
+ "typescript": "^5.9.3",
+ "vitest": "^3.2.4"
+ },
+ "dependencies": {
+ "@lowerdeck/error": "^1.0.5"
+ }
+}
\ No newline at end of file
diff --git a/packages/api-mux/src/index.ts b/packages/api-mux/src/index.ts
new file mode 100644
index 0000000..fa5d671
--- /dev/null
+++ b/packages/api-mux/src/index.ts
@@ -0,0 +1 @@
+export * from './mux';
diff --git a/packages/api-mux/src/mux.ts b/packages/api-mux/src/mux.ts
new file mode 100644
index 0000000..582185b
--- /dev/null
+++ b/packages/api-mux/src/mux.ts
@@ -0,0 +1,54 @@
+import { notFoundError } from '@lowerdeck/error';
+
+export let apiMux = (
+ services: {
+ domains?: string[];
+ methods?: string[];
+ endpoint: { path: string | string[]; fetch: (req: any) => Promise; exact?: boolean };
+ }[],
+ fallback?: (req: any, server: any) => Promise
+) => {
+ let servicesWithRegex = services.flatMap(({ domains, endpoint, methods }) =>
+ (Array.isArray(endpoint.path) ? endpoint.path : [endpoint.path]).map(path => {
+ return {
+ path,
+ domains,
+ endpoint,
+ exact: endpoint.exact,
+ methods: methods?.map(m => m.toUpperCase())
+ };
+ })
+ );
+
+ return (req: Request, server: any) => {
+ let url = new URL(req.url);
+ let host = (req.headers.get('x-host') ?? req.headers.get('host') ?? url.hostname).split(
+ ':'
+ )[0];
+ url.host = host;
+
+ if (url.pathname == '/ping') {
+ return new Response('OK') as any;
+ }
+
+ for (let { domains, endpoint, path, methods, exact } of servicesWithRegex) {
+ if (domains && !domains.includes(host)) continue;
+
+ if (
+ (url.pathname == path || (!exact && url.pathname.startsWith(`${path}/`))) &&
+ (!methods || methods.includes(req.method))
+ ) {
+ return endpoint.fetch(req);
+ }
+ }
+
+ if (fallback) return fallback(req, server);
+
+ return new Response(JSON.stringify(notFoundError('route').toResponse()), {
+ status: 404,
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }) as any;
+ };
+};
diff --git a/packages/api-mux/tsconfig.json b/packages/api-mux/tsconfig.json
new file mode 100644
index 0000000..c62d3e3
--- /dev/null
+++ b/packages/api-mux/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@lowerdeck/tsconfig/base.json",
+ "exclude": [
+ "dist"
+ ],
+ "include": [
+ "src"
+ ],
+ "compilerOptions": {
+ "outDir": "dist"
+ }
+}
\ No newline at end of file
diff --git a/packages/api-mux/tsup.config.js b/packages/api-mux/tsup.config.js
new file mode 100644
index 0000000..4e2a3d6
--- /dev/null
+++ b/packages/api-mux/tsup.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from 'tsup';
+
+export default defineConfig({
+ entry: ['src/index.ts'],
+ format: ['esm', 'cjs'],
+ splitting: false,
+ sourcemap: true,
+ clean: true,
+ bundle: true,
+ dts: true
+});