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. + +
+ Built with ❤️ by Metorial +
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 +});