├── .github └── FUNDING.yml ├── test ├── hint.test.ts ├── router.test.ts ├── no_handler_test.ts ├── trie.test.ts ├── server.ts ├── websockets.test.ts └── server.test.ts ├── .gitignore ├── index.js ├── index.ts ├── src ├── index.ts ├── server │ ├── websocket.ts │ ├── response.ts │ ├── request.ts │ ├── trie-tree.ts │ └── server.ts ├── utils │ ├── chain.ts │ └── base64.ts └── router │ └── router.ts ├── tsconfig.json ├── package.json ├── LICENSE └── readme.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | 2 | patreon: ruidot 3 | -------------------------------------------------------------------------------- /test/hint.test.ts: -------------------------------------------------------------------------------- 1 | import server from '../src'; 2 | 3 | const app = server(); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | node_modules.bun 4 | bun.lockb -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import server from "./src"; 4 | 5 | export default server; 6 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import server from "./src"; 4 | 5 | export default server; 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { server } from "./server/server"; 4 | 5 | export default server; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "sourceRoot": "lib", 5 | "outDir": "dist", 6 | "lib": [ 7 | "ESNext" 8 | ], 9 | "rootDirs": [ /* List of root folders whose combined content represents the structure of the project at runtime. */ 10 | "./src", 11 | "./test" 12 | ], 13 | "target": "esnext", 14 | "module": "commonjs", 15 | "esModuleInterop": true, 16 | // "bun-types" is the important part 17 | "types": [ 18 | "bun-types" 19 | ], 20 | "moduleResolution": "node", 21 | } 22 | } -------------------------------------------------------------------------------- /src/server/websocket.ts: -------------------------------------------------------------------------------- 1 | import { ServerWebSocket } from "bun"; 2 | 3 | export type RestSocketHandler = ( 4 | ws: ServerWebSocket, 5 | message: string | Buffer, 6 | ) => void | Promise 7 | 8 | type OpenHandler = ( 9 | ws: ServerWebSocket, 10 | ) => void | Promise; 11 | 12 | type DrainHandler = ( 13 | ws: ServerWebSocket 14 | ) => void | Promise; 15 | 16 | type CloseHandler = ( 17 | ws: ServerWebSocket, 18 | code: number, 19 | reason: string, 20 | ) => void | Promise; 21 | 22 | export interface ExtraHandler { 23 | open?: OpenHandler | undefined; 24 | close?: CloseHandler | undefined; 25 | drain?: DrainHandler | undefined; 26 | } -------------------------------------------------------------------------------- /test/router.test.ts: -------------------------------------------------------------------------------- 1 | import Server from '../src'; 2 | import { describe, it, expect, beforeAll, afterAll } from "bun:test"; 3 | import { Router } from './router/router'; 4 | 5 | const app = Server(); 6 | const router = app.router(); 7 | 8 | router.get('/', (req, res) => { 9 | res.status(200).send('GET /route') 10 | }) 11 | 12 | router.post('/', (req, res) => { 13 | res.status(200).send('POST /route') 14 | }) 15 | 16 | router.patch('/', (req, res) => { 17 | res.status(200).send('PATCH /route') 18 | }) 19 | 20 | router.put('/', (req, res) => { 21 | res.status(200).send('PUT /route') 22 | }) 23 | 24 | router.delete('/', (req, res) => { 25 | res.status(200).send('DELETE /route') 26 | }) 27 | 28 | router.options('/', (req, res) => { 29 | res.status(200).send('OPTIONS /route') 30 | }) 31 | 32 | router.head('/', (req, res) => { 33 | res.status(200).send('HEAD /route') 34 | }) 35 | 36 | export default router -------------------------------------------------------------------------------- /test/no_handler_test.ts: -------------------------------------------------------------------------------- 1 | import server from '../src'; 2 | import { describe, it, expect, beforeAll, afterAll } from "bun:test"; 3 | 4 | const app = server(); 5 | 6 | app.get('/test', (req, res) => { 7 | res.status(200).send('GET /'); 8 | }); 9 | 10 | //add error handler 11 | app.use((req, res, next, err) => { 12 | res.status(500).send('Err /err'); 13 | }); 14 | 15 | const URL_PORT = 5555; 16 | const BASE_URL = `http://localhost:${URL_PORT}`; 17 | 18 | describe('no handler test', () => { 19 | it('GET', async () => { 20 | const server = app.listen(URL_PORT, () => { 21 | console.log(`App is listening on port ${URL_PORT}`); 22 | }); 23 | try { 24 | const res = await fetch(BASE_URL); 25 | expect(res.status).toBe(404); 26 | expect(await res.text()).toBe('GET / with a 404') 27 | } catch (e) { 28 | throw e; 29 | } finally { 30 | server.stop(); 31 | } 32 | }); 33 | }) 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bunrest", 3 | "version": "1.3.7", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "description": "An express-like API for bun http server", 7 | "scripts": { 8 | "start": "bun run index.js", 9 | "test": "bun test" 10 | }, 11 | "devDependencies": { 12 | "@types/cors": "^2.8.13", 13 | "@typescript-eslint/eslint-plugin": "^5.32.0", 14 | "@typescript-eslint/parser": "^5.32.0", 15 | "bun-types": "latest" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/lau1944/bunrest" 20 | }, 21 | "keywords": [ 22 | "bunrest", 23 | "bun", 24 | "express", 25 | "http", 26 | "rest", 27 | "restful", 28 | "web", 29 | "app", 30 | "server", 31 | "framework", 32 | "api" 33 | ], 34 | "author": "Jimmy leo", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/lau1944/bunrest/issues" 38 | }, 39 | "homepage": "https://github.com/lau1944/bunrest" 40 | } 41 | -------------------------------------------------------------------------------- /test/trie.test.ts: -------------------------------------------------------------------------------- 1 | // import { describe, it, expect } from "bun:test"; 2 | // import { TrieTree } from "../src/server/trie-tree"; 3 | // 4 | // describe('data container test', () => { 5 | // it('trie tree test', () => { 6 | // const tree: TrieTree = new TrieTree(); 7 | // tree.insert('GET-/', 1); 8 | // expect(tree.get('GET-/')?.node.getValue()).toBe(1); 9 | // expect(tree.get('GET-/xx')?.node?.getValue()).toBe(undefined); 10 | // 11 | // tree.insert('GET-/test1/:id', 2); 12 | // expect(tree.get('GET-/test1/xxx')?.node.getValue()).toBe(2); 13 | // 14 | // tree.insert('POST-/test2', 3); 15 | // expect(tree.get('POST-/test2')?.node.getValue()).toBe(3); 16 | // 17 | // tree.insert('POST-/test1/test1', 4); 18 | // expect(tree.get('POST-/test1/test1')?.node.getValue()).toBe(4); 19 | // 20 | // tree.insert('POST-/test2/test2', 5); 21 | // expect(tree.get('POST-/test2/test2')?.node.getValue()).toBe(5); 22 | // }); 23 | // }) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 1au 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils/chain.ts: -------------------------------------------------------------------------------- 1 | import { BunRequest, MiddlewareFunc } from "../server/request"; 2 | import { BunResponse } from "../server/response"; 3 | 4 | export class Chain { 5 | private index: number = 0; 6 | private req: BunRequest; 7 | private res: BunResponse; 8 | private middlewares: MiddlewareFunc[]; 9 | private resolve: () => void; 10 | 11 | constructor(req: BunRequest, res: BunResponse, middlewares: MiddlewareFunc[]) { 12 | this.req = req; 13 | this.res = res; 14 | this.middlewares = middlewares; 15 | } 16 | 17 | private async processNext(err: Error) { 18 | if (err) { 19 | throw err; 20 | } 21 | if (this.index < this.middlewares.length) { 22 | const middleware = this.middlewares[this.index++]; 23 | let nextCalled = false; 24 | 25 | const nextWrapper = (err: Error) => { 26 | nextCalled = true; 27 | this.processNext(err); 28 | }; 29 | 30 | await middleware(this.req, this.res, nextWrapper); 31 | 32 | if (!nextCalled) { 33 | this.resolve(); 34 | } 35 | } else { 36 | this.resolve(); 37 | } 38 | } 39 | 40 | public run(): Promise { 41 | return new Promise((resolve) => { 42 | this.resolve = resolve; 43 | this.processNext(); 44 | }); 45 | } 46 | 47 | public isFinish(): boolean { 48 | return this.index === this.middlewares.length; 49 | } 50 | 51 | public isReady(): boolean { 52 | return !this.isFinish(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/server/response.ts: -------------------------------------------------------------------------------- 1 | export class BunResponse { 2 | private response: Response; 3 | private options: ResponseInit = {}; 4 | 5 | status(code: number): BunResponse { 6 | this.options.status = code; 7 | return this; 8 | } 9 | 10 | option(option: ResponseInit): BunResponse { 11 | this.options = Object.assign(this.options, option); 12 | return this; 13 | } 14 | 15 | statusText(text: string): BunResponse { 16 | this.options.statusText = text; 17 | return this; 18 | } 19 | 20 | json(body: any): void { 21 | this.response = Response.json(body, this.options); 22 | } 23 | 24 | send(body: any): void { 25 | this.response = new Response(body, this.options); 26 | } 27 | 28 | redirect(url: string, status: number = 302): void { 29 | this.response = Response.redirect(url, status); 30 | } 31 | 32 | // nodejs way to set headers 33 | setHeader(key: string, value: any) { 34 | if (!key || !value) { 35 | throw new Error('Headers key or value should not be empty'); 36 | } 37 | 38 | const headers = this.options.headers; 39 | if (!headers) { 40 | this.options.headers = { keys: value }; 41 | } 42 | this.options.headers[key] = value; 43 | return this; 44 | } 45 | 46 | // nodejs way to get headers 47 | getHeader() { 48 | return this.options.headers; 49 | } 50 | 51 | headers(header: HeadersInit): BunResponse { 52 | this.options.headers = header; 53 | return this; 54 | } 55 | 56 | getResponse(): Response { 57 | return this.response; 58 | } 59 | 60 | isReady(): boolean { 61 | return !!this.response; 62 | } 63 | } -------------------------------------------------------------------------------- /test/server.ts: -------------------------------------------------------------------------------- 1 | // import Server from "../index"; 2 | // import cors from "cors"; 3 | 4 | // const app = Server(); 5 | // const router = app.router(); 6 | 7 | // // var whitelist = ["localhost:3000"]; 8 | // // var corsOptions = { 9 | // // origin: function (origin, callback) { 10 | // // if (whitelist.includes(origin)) { 11 | // // callback(null, true); 12 | // // } else { 13 | // // callback(new Error("Not allowed by CORS")); 14 | // // } 15 | // // }, 16 | // // }; 17 | 18 | // //app.use(cors(corsOptions)); 19 | 20 | // app.get("/user", (req, res) => { 21 | // res.status(200).json(req.body); 22 | // }); 23 | 24 | // app.post('/', (req, res) => { 25 | // res.status(200).send("app post") 26 | // }) 27 | 28 | // app.use((req,res, next) => { 29 | // console.log('middle ware'); 30 | // next(); 31 | // }) 32 | 33 | // // app.get('/', (req, res) => { 34 | // // console.log('not handle') 35 | // // // res.status(200).send("app get") 36 | // // }) 37 | // router.use((req, res, next) => { 38 | // console.log('router middleware'); 39 | // }) 40 | 41 | // router.get('/', (req, res) => { 42 | // res.status(200).send("route get") 43 | // }) 44 | 45 | // router.post('/', (req, res) => { 46 | // res.status(200).send("router post") 47 | // }) 48 | // // app.use((req, res, next) => { 49 | // // res.status(400).send('Yoy') 50 | // // }) 51 | 52 | // // app.use((req, res, next) => { 53 | // // res.status(400).send('Not found') 54 | // // }) 55 | 56 | // app.use('/', router); 57 | 58 | // // app.use((req, res, next, err) => { 59 | // // res.status(500).send('Error happened'); 60 | // // }); 61 | 62 | // app.listen(3000, () => { 63 | // console.log("Running on port 3000"); 64 | // }); 65 | import server from "../index"; 66 | 67 | const app = server() 68 | const router = app.router() 69 | 70 | const URL_PORT = 5555; 71 | 72 | app.get("/user", (req, res) => { 73 | res.status(200).json(req.body); 74 | }); 75 | 76 | router.get('/', async (req, res) => { 77 | //const data = await (await fetch('https://www.fishwatch.gov/api/species')).json() 78 | res.status(200).json({ message: 'sdsdsd'}) 79 | }) 80 | 81 | router.options('/test', async (req, res) => { 82 | //const data = await (await fetch('https://www.fishwatch.gov/api/species')).json() 83 | res.status(200).json({ message: 'hihi'}) 84 | }) 85 | 86 | app.use('/', router); 87 | 88 | app.listen(URL_PORT, () => { 89 | console.log('App is listening on port 3000') 90 | }) 91 | -------------------------------------------------------------------------------- /test/websockets.test.ts: -------------------------------------------------------------------------------- 1 | import server from '../src'; 2 | import { describe, it, expect} from "bun:test"; 3 | 4 | const app = server(); 5 | 6 | app.get('*', (req, res) => { 7 | res.status(200).send('GET /'); 8 | }); 9 | 10 | app.ws<{data: string}>((ws, msg) => { 11 | ws.send(ws.data.data + "|" + msg) 12 | }, { 13 | open: (ws) => { 14 | console.log('Websocket is turned on') 15 | }, close: (ws) => { 16 | console.log('Websocket is closed') 17 | }, drain: (ws) => { 18 | console.log('Websocket is drained') 19 | } 20 | }, 21 | (req) => ({data: "socket-data"}) 22 | ) 23 | 24 | const URL_PORT = 5555; 25 | const BASE_URL = `http://localhost:${URL_PORT}`; 26 | 27 | describe('http with websockets on test', () => { 28 | it('GET', async () => { 29 | const server = app.listen(URL_PORT, () => { 30 | console.log(`App is listening on port ${URL_PORT}`); 31 | }); 32 | try { 33 | const res = await fetch(BASE_URL); 34 | expect(res.status).toBe(200); 35 | expect(await res.text()).toBe('GET /') 36 | } catch (e) { 37 | throw e; 38 | } finally { 39 | server.stop(); 40 | } 41 | }); 42 | }) 43 | 44 | describe('websocket test', () => { 45 | it(('ws'), async () => { 46 | let server = app.listen(URL_PORT, () => { 47 | console.log(`App is listening on port ${URL_PORT}`); 48 | }); 49 | try { 50 | const msg = 'Hello world' 51 | expect(await new Promise((resolve) => { 52 | const socket = new WebSocket(`ws://localhost:${URL_PORT}`); 53 | // message is received 54 | socket.addEventListener("message", event => { 55 | resolve(event.data); 56 | }); 57 | // socket opened 58 | socket.addEventListener("open", event => { 59 | console.log('Open') 60 | socket.send(msg) 61 | }); 62 | // socket closed 63 | socket.addEventListener("close", event => { 64 | console.log('Close ' + event.code); 65 | resolve(false); 66 | }); 67 | 68 | // error handler 69 | socket.addEventListener("error", event => { 70 | console.log('Error') 71 | resolve(false); 72 | }); 73 | })).toBe("socket-data|" + msg); 74 | } catch (e) { 75 | throw e 76 | } finally { 77 | server.stop() 78 | } 79 | }) 80 | }) -------------------------------------------------------------------------------- /src/server/request.ts: -------------------------------------------------------------------------------- 1 | import { BunResponse } from "./response"; 2 | import { TrieTree } from "./trie-tree"; 3 | 4 | export type Handler = ( 5 | req: BunRequest, 6 | res: BunResponse, 7 | next?: (err?: Error) => {}, 8 | err?: Error 9 | ) => void | Promise; 10 | 11 | export type Route = ( 12 | handler: Handler, 13 | middlewareFuncs: Handler[] 14 | ) => void | Promise; 15 | 16 | export type MiddlewareFunc = ( 17 | req: Request, 18 | res: BunResponse, 19 | next: (err?: Error) => {} 20 | ) => void; 21 | 22 | export type RequestMethodType = 'GET' | 'PUT' | 'POST' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD'; 23 | 24 | export type RequestHandler = (path: string, ...handlers: Handler[]) => void; 25 | 26 | export type Middleware = { 27 | path: string; 28 | middlewareFunc: Handler; 29 | }; 30 | 31 | export interface RequestMethod { 32 | get: RequestHandler; 33 | post: RequestHandler; 34 | patch: RequestHandler; 35 | put: RequestHandler; 36 | delete: RequestHandler; 37 | options: RequestHandler; 38 | head: RequestHandler; 39 | } 40 | 41 | export interface BunRequest { 42 | method: string; 43 | request: Request; 44 | path: string; 45 | headers?: { [key: string]: any }; 46 | params?: { [key: string]: any }; 47 | query?: { [key: string]: any }; 48 | body?: { [key: string]: any } | string | undefined; 49 | blob?: any; 50 | originalUrl: string; 51 | } 52 | 53 | export interface SSLOptions { 54 | reusePort: boolean; 55 | keyFile: string; 56 | certFile: string; 57 | passphrase?: string; 58 | caFile?: string; 59 | dhParamsFile?: string; 60 | 61 | /** 62 | * This sets `OPENSSL_RELEASE_BUFFERS` to 1. 63 | * It reduces overall performance but saves some memory. 64 | * @default false 65 | */ 66 | lowMemoryMode?: boolean; 67 | } 68 | 69 | /** 70 | * request method mapper 71 | */ 72 | export interface RequestMapper { 73 | get?: TrieTree; 74 | post?: TrieTree; 75 | patch?: TrieTree; 76 | put?: TrieTree; 77 | delete?: TrieTree; 78 | options?: TrieTree; 79 | head?: TrieTree; 80 | } 81 | 82 | export interface RequestTuple { 83 | path: string; 84 | handler: Handler; 85 | } 86 | 87 | /** 88 | * Router method mapper 89 | */ 90 | export interface RouteRequestMapper { 91 | get?: Array; 92 | post?: Array; 93 | patch?: Array; 94 | put?: Array; 95 | delete?: Array; 96 | options?: Array; 97 | head?: Array; 98 | } 99 | 100 | export type RequestMapFunc = (method: string, path: string, handler: Handler) => void 101 | -------------------------------------------------------------------------------- /src/router/router.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Handler, 3 | Middleware, 4 | RequestMapFunc, 5 | RequestMapper, 6 | RequestMethod, 7 | RequestTuple, 8 | RouteRequestMapper, 9 | } from "../server/request"; 10 | import path from "path"; 11 | // import { encodeBase64 } from "../utils/base64"; 12 | 13 | export type RouterMeta = { 14 | globalPath: string; 15 | request: Map; 16 | middlewares: Map; 17 | }; 18 | 19 | export class Router implements RequestMethod { 20 | private readonly requestMap: RequestMapper; 21 | private readonly middlewares: Middleware[]; 22 | private readonly requestMapFunc: RequestMapFunc; 23 | private localRequestMap: RouteRequestMapper = {}; 24 | private localMiddlewares: Middleware[] = []; 25 | 26 | constructor(requestMap: RequestMapper, middlewares: Middleware[], requestMapFunc: RequestMapFunc) { 27 | this.requestMap = requestMap; 28 | this.requestMapFunc = requestMapFunc; 29 | this.middlewares = middlewares; 30 | } 31 | 32 | get(path: string, ...handlers: Handler[]) { 33 | this.delegate(path, "GET", handlers); 34 | } 35 | 36 | post(path: string, ...handlers: Handler[]) { 37 | this.delegate(path, "POST", handlers); 38 | } 39 | 40 | patch(path: string, ...handlers: Handler[]) { 41 | this.delegate(path, "PATCH", handlers); 42 | } 43 | 44 | options(path: string, ...handlers: Handler[]) { 45 | this.delegate(path, "OPTIONS", handlers); 46 | } 47 | 48 | put(path: string, ...handlers: Handler[]) { 49 | this.delegate(path, "PUT", handlers); 50 | } 51 | 52 | delete(path: string, ...handlers: Handler[]) { 53 | this.delegate(path, "DELETE", handlers); 54 | } 55 | 56 | head(path: string, ...handlers: Handler[]) { 57 | this.delegate(path, "HEAD", handlers); 58 | } 59 | 60 | use(middleware: Handler) { 61 | this.localMiddlewares.push(middleware); 62 | } 63 | 64 | attach(globalPath: string) { 65 | for (const k in this.localRequestMap) { 66 | const method = k; 67 | const reqArr: Array = this.localRequestMap[k]; 68 | reqArr.forEach((v, _) => { 69 | this.requestMapFunc.apply(this, [method, path.join(globalPath, v.path), v.route.handler, v.route.middlewareFuncs]); 70 | }); 71 | } 72 | } 73 | 74 | private delegate(localPath: string, method: string, handlers: Handler[]) { 75 | if (localPath === '/') { 76 | localPath = '' 77 | } 78 | 79 | if (handlers.length < 1) return; 80 | // Split the array 81 | const middlewares = handlers.slice(0, -1); // Array with all elements except the last one 82 | const handler = handlers[handlers.length - 1]; // Array with only the last element 83 | 84 | this.submitToMap(method.toLowerCase(), localPath, handler, middlewares); 85 | } 86 | 87 | private submitToMap(method: string, path: string, handler: Handler, middlewares: Middleware) { 88 | let targetMap: RequestTuple[] = this.localRequestMap[method]; 89 | if (!targetMap) { 90 | this.localRequestMap[method] = []; 91 | targetMap = this.localRequestMap[method]; 92 | } 93 | 94 | const route = { 95 | handler: handler, 96 | middlewareFuncs: middlewares, 97 | } 98 | 99 | targetMap.push({ 100 | path, 101 | route, 102 | }); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/server/trie-tree.ts: -------------------------------------------------------------------------------- 1 | import { Handler, Route, RequestTuple, RouteRequestMapper } from "./request"; 2 | 3 | //import { encodeBase64, decodeBase64 } from "../utils/base64"; 4 | export class TrieTree { 5 | private readonly root: Node; 6 | 7 | constructor() { 8 | this.root = new Node(); 9 | } 10 | 11 | get(path: string): TrieLeaf { 12 | // const paths = this.validate(path); 13 | // paths.shift(); 14 | const paths = path.split("/"); 15 | const node: Node = this.root; 16 | const params = {}; 17 | return { 18 | routeParams: params, 19 | node: this.dig(node, paths, params), 20 | }; 21 | } 22 | 23 | insert(path: string, value: v) { 24 | // const paths = this.validate(path); 25 | // // remove the first empty string 26 | // paths.shift(); 27 | if (path === '*') { 28 | path = '/'; 29 | } 30 | const paths = path.split("/"); 31 | let node: Node = this.root; 32 | let index = 0; 33 | while (index < paths.length) { 34 | const children = node.getChildren(); 35 | const currentPath = paths[index]; 36 | let target = children.find((e) => e.getPath() === currentPath); 37 | if (!target) { 38 | target = new Node(currentPath); 39 | children.push(target); 40 | } 41 | 42 | node = target; 43 | ++index; 44 | } 45 | // insert handler to node 46 | node.insertChild(value); 47 | } 48 | 49 | private dig( 50 | node: Node, 51 | paths: string[], 52 | params: { [key: string]: any } 53 | ): Node | null { 54 | if (paths.length === 0) { 55 | return node; 56 | } 57 | 58 | const target = node 59 | .getChildren() 60 | .filter((e) => e.getPath() === paths[0] || e.getPath().includes(":")); 61 | 62 | if (target.length === 0) { 63 | return null; 64 | } 65 | 66 | let next: Node = null 67 | for (let i = 0; i < target.length; ++i) { 68 | const e = target[i]; 69 | if (e.getPath().startsWith(":")) { 70 | const routeParams = e.getPath().replace(":", ""); 71 | params[routeParams] = paths[0]; 72 | } 73 | 74 | paths.shift(); 75 | next = this.dig(e, paths, params); 76 | 77 | if (next) { 78 | return next; 79 | } 80 | } 81 | return next; 82 | } 83 | 84 | // private validate(path: string) { 85 | // // if (!path.includes("~")) { 86 | // // throw new Error("Path should contains a separator ~"); 87 | // // } 88 | 89 | // //const [method, httpPath] = path.split("~"); 90 | // const paths = path.split("/"); 91 | // return paths; 92 | // } 93 | } 94 | 95 | export interface TrieLeaf { 96 | node: Node | null; 97 | routeParams: { [key: string]: any }; 98 | } 99 | 100 | // node of trie tree 101 | class Node { 102 | private readonly path?: string; 103 | private readonly handlers: Route = {}; 104 | private readonly children: Node[] = []; 105 | 106 | constructor(path?: string) { 107 | this.path = path; 108 | } 109 | 110 | insertChild(handlers: Route) { 111 | this.handlers = handlers; 112 | } 113 | 114 | getChildren(): Node[] { 115 | return this.children; 116 | } 117 | 118 | getHandler(): Handler { 119 | return this.handlers.handler; 120 | } 121 | 122 | getMiddlewares(): Handler[] { 123 | return this.handlers.middlewareFuncs 124 | } 125 | 126 | getPath(): string { 127 | return this.path; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 | Logo 3 |
4 |

5 | 6 | # 🧄 bunrest 7 | 8 | [![NPM Version][npm-version-image]][npm-url] 9 | [![CodeFactor](https://www.codefactor.io/repository/github/lau1944/bunrest/badge/main)](https://www.codefactor.io/repository/github/lau1944/bunrest/overview/main) 10 | ![NPM Downloads][npm-downloads-image] 11 | 12 | ## What is bunrest 👀 13 | 14 | ### bunrest is an ExpressJs-like API for [bun](https://github.com/oven-sh/bun) http server. 15 | 16 | ## Features 17 | 18 | 1. ⚡ BLAZING FAST. Bun is super fast... 19 | 20 | 2. 0️⃣ dependencies, work seamlessly with Bun 21 | 22 | 3. 0️⃣ learning curve. If you know ExpressJs, you can start a bun server. 23 | 24 | ## Table of Contents 25 | 26 | - [Set up](#get-started) 27 | - [Usage](#usage) 28 | - [Router](#router) 29 | - [Middlewares](#middlewares) 30 | - [Error handling](#error-handling) 31 | - [Request and Response object](#request-and-response-object) 32 | - [Websocket](#websocket) 33 | 34 | 35 | ### Get started 36 | 37 | To download bun 38 | 39 | ```shell 40 | curl -fsSL https://bun.sh/install | bash 41 | ``` 42 | 43 | To create a bun project 44 | 45 | ```shell 46 | bun init 47 | ``` 48 | 49 | This will create a blank bun project 50 | 51 | see reference [here](https://github.com/oven-sh/bun#bun-create) 52 | 53 | ### Server set up 54 | 55 | Download the package 56 | 57 | ```shell 58 | bun install bunrest 59 | ``` 60 | 61 | 62 | ```js 63 | import server from "bunrest"; 64 | const app = server(); 65 | ``` 66 | 67 | ### Usage 68 | 69 | After that, you can write http method just like on `express` 70 | 71 | ```js 72 | app.get('/test', (req, res) => { 73 | res.status(200).json({ message: req.query }); 74 | }); 75 | 76 | app.put('/test/:id', (req, res) => { 77 | res.status(200).json({ message: req.params.id }); 78 | }); 79 | 80 | app.post('/test/:id/:name', (req, res) => { 81 | res.status(200).json({ message: req.params }); 82 | }); 83 | ``` 84 | 85 | ### Router 86 | The same as above, we create a router by calling `server.Router()` 87 | 88 | After creation, we attach the router to server by calling `server.use(your_router_reference)` 89 | 90 | ```js 91 | // add router 92 | const router = app.router(); 93 | 94 | router.get('/test', (req, res) => { 95 | res.status(200).json({ message: 'Router succeed' }); 96 | }) 97 | 98 | router.post('/test', (req, res) => { 99 | res.status(200).json({ message: 'Router succeed' }); 100 | }) 101 | 102 | router.put('/test', (req, res) => { 103 | res.status(200).json({ message: 'Router succeed' }); 104 | }) 105 | 106 | app.use('/your_route_path', router); 107 | ``` 108 | 109 | ### Middlewares 110 | 111 | We have two ways to add middlewares 112 | 113 | 1. `use` : Simply call `use` to add the middleware function. 114 | 115 | 2. Add middleware at the middle of your request function parameters. 116 | 117 | ```js 118 | // use 119 | app.use((req, res, next) => { 120 | console.log("middlewares called"); 121 | // to return result 122 | res.status(500).send("server denied"); 123 | }); 124 | 125 | app.use((req, res, next) => { 126 | console.log("middlewares called"); 127 | // to call next middlewares 128 | next(); 129 | }) 130 | 131 | // or you can add the middlewares this way 132 | app.get('/user', 133 | (req, res, next) => { 134 | // here to handle middleware for path '/user' 135 | }, 136 | (req, res) => { 137 | res.status(200).send('Hello'); 138 | }); 139 | ``` 140 | 141 | ### Error handling 142 | 143 | To add a global handler, it's really similar to express but slightly different. The fourth argument is the error object, but I only get `[native code]` from error object, this might related to bun. 144 | 145 | ```js 146 | app.use((req, res, next, err) => { 147 | res.status(500).send('Error happened'); 148 | }); 149 | 150 | ``` 151 | 152 | At this time, if we throw an error on default path `/` 153 | 154 | ```js 155 | app.get('/', (req, res) => { 156 | throw new Error('Oops'); 157 | }) 158 | ``` 159 | 160 | It will call the `error handler callback function` and return a `response`. 161 | But if we have not specified a `response` to return, a `error page` will be displayed on the browser on debug mode, check more on [bun error handling](https://github.com/oven-sh/bun#error-handling) 162 | 163 | 164 | ### Start the server, listen to port 165 | 166 | ```js 167 | app.listen(3000, () => { 168 | console.log('App is listening on port 3000'); 169 | }); 170 | ``` 171 | 172 |
173 | 174 | ### Request and Response object 175 | 176 | To simulate the `ExpressJs` API, the default `request` and `response` object on `bunjs` is not ideal. 177 | 178 | On `bunrest`, we create our own `request` and `response` object, here is the blueprint of these two objects. 179 | 180 | 181 | Request interface 182 | 183 | ```js 184 | export interface BunRequest { 185 | method: string; 186 | request: Request; 187 | path: string; 188 | header?: { [key: string]: any }; 189 | params?: { [key: string]: any }; 190 | query?: { [key: string]: any }; 191 | body?: { [key: string]: any }; 192 | blob?: any; 193 | } 194 | ``` 195 | 196 | Response interface 197 | ```js 198 | export interface BunResponse { 199 | status(code: number): BunResponse; 200 | option(option: ResponseInit): BunResponse; 201 | statusText(text: string): BunResponse; 202 | json(body: any): void; 203 | send(body: any): void; 204 | // nodejs way to set headers 205 | setHeader(key: string, value: any); 206 | // nodejs way to get headers 207 | getHeader();this.options.headers; 208 | headers(header: HeadersInit): BunResponse; 209 | getResponse(): Response; 210 | isReady(): boolean;turn !!this.response; 211 | } 212 | ``` 213 | 214 | The `req` and `res` arguments inside every handler function is with the type of `BunRequest` and `BunResponse`. 215 | 216 | So you can use it like on Express 217 | 218 | ```js 219 | const handler = (req, res) => { 220 | const { name } = req.params; 221 | const { id } = req.query; 222 | res.setHeader('Content-Type', 'application/text'); 223 | res.status(200).send('No'); 224 | } 225 | ``` 226 | 227 | # websocket 228 | 229 |
To handle websocket request, just a few steps to do
230 | 231 | ```js 232 | app.ws<{str: string}>((ws, msg) => { 233 | // here to handle incoming message 234 | ws.send(msg) 235 | // get web socket data 236 | console.log(ws.data) 237 | }, { 238 | open: (ws) => { 239 | console.log('Websocket is turned on') 240 | }, close: (ws) => { 241 | console.log('Websocket is closed') 242 | }, drain: (ws) => { 243 | console.log('Websocket is drained') 244 | } 245 | }, 246 | (req) => ({str: "socket-data"})) 247 | ``` 248 | 249 |
To connect to your websocket server
250 | 251 | ```js 252 | const socket = new WebSocket("ws://localhost:3000"); 253 | const msg = 'Hello world' 254 | // message is received 255 | socket.addEventListener("message", event => { 256 | console.log(event.data) 257 | }); 258 | 259 | // socket opened 260 | socket.addEventListener("open", event => { 261 | console.log('Open') 262 | // here to send message 263 | socket.send(msg) 264 | }); 265 | 266 | // socket closed 267 | socket.addEventListener("close", event => { 268 | console.log('Close') 269 | }); 270 | 271 | // error handler 272 | socket.addEventListener("error", event => { 273 | console.log('Error') 274 | }); 275 | ``` 276 | 277 | [npm-url]: https://www.npmjs.com/package/bunrest 278 | [npm-version-image]: https://badgen.net/npm/v/bunrest 279 | [npm-downloads-image]: https://badgen.net/npm/dm/bunrest 280 | -------------------------------------------------------------------------------- /src/utils/base64.ts: -------------------------------------------------------------------------------- 1 | // /** 2 | // * Purpose of this file is to provide a reusable base64 encoder/decoder, that will encode user paths so that they do not conflict with the internal delimiter used, as seen with the '~' character. Before it was '-', and no routes with '-' were allowed. This is allows for more flexibility in the routes. 3 | // * 4 | // * Decoding of base64 is broken, so I will use the mozilla implementation for now. 5 | // */ 6 | 7 | // // Language: typescript 8 | 9 | 10 | // // Array of bytes to Base64 string decoding 11 | // function b64ToUint6(nChr) { 12 | // return nChr > 64 && nChr < 91 13 | // ? nChr - 65 14 | // : nChr > 96 && nChr < 123 15 | // ? nChr - 71 16 | // : nChr > 47 && nChr < 58 17 | // ? nChr + 4 18 | // : nChr === 43 19 | // ? 62 20 | // : nChr === 47 21 | // ? 63 22 | // : 0; 23 | // } 24 | 25 | // function base64DecToArr(sBase64, nBlocksSize) { 26 | // const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, ""); 27 | // const nInLen = sB64Enc.length; 28 | // const nOutLen = nBlocksSize 29 | // ? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize 30 | // : (nInLen * 3 + 1) >> 2; 31 | // const taBytes = new Uint8Array(nOutLen); 32 | 33 | // let nMod3; 34 | // let nMod4; 35 | // let nUint24 = 0; 36 | // let nOutIdx = 0; 37 | // for (let nInIdx = 0; nInIdx < nInLen; nInIdx++) { 38 | // nMod4 = nInIdx & 3; 39 | // nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (6 * (3 - nMod4)); 40 | // if (nMod4 === 3 || nInLen - nInIdx === 1) { 41 | // nMod3 = 0; 42 | // while (nMod3 < 3 && nOutIdx < nOutLen) { 43 | // taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255; 44 | // nMod3++; 45 | // nOutIdx++; 46 | // } 47 | // nUint24 = 0; 48 | // } 49 | // } 50 | 51 | // return taBytes; 52 | // } 53 | 54 | // /* Base64 string to array encoding */ 55 | // function uint6ToB64(nUint6) { 56 | // return nUint6 < 26 57 | // ? nUint6 + 65 58 | // : nUint6 < 52 59 | // ? nUint6 + 71 60 | // : nUint6 < 62 61 | // ? nUint6 - 4 62 | // : nUint6 === 62 63 | // ? 43 64 | // : nUint6 === 63 65 | // ? 47 66 | // : 65; 67 | // } 68 | 69 | // function base64EncArr(aBytes) { 70 | // let nMod3 = 2; 71 | // let sB64Enc = ""; 72 | 73 | // const nLen = aBytes.length; 74 | // let nUint24 = 0; 75 | // for (let nIdx = 0; nIdx < nLen; nIdx++) { 76 | // nMod3 = nIdx % 3; 77 | // if (nIdx > 0 && ((nIdx * 4) / 3) % 76 === 0) { 78 | // sB64Enc += "\r\n"; 79 | // } 80 | 81 | // nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24); 82 | // if (nMod3 === 2 || aBytes.length - nIdx === 1) { 83 | // sB64Enc += String.fromCodePoint( 84 | // uint6ToB64((nUint24 >>> 18) & 63), 85 | // uint6ToB64((nUint24 >>> 12) & 63), 86 | // uint6ToB64((nUint24 >>> 6) & 63), 87 | // uint6ToB64(nUint24 & 63) 88 | // ); 89 | // nUint24 = 0; 90 | // } 91 | // } 92 | // return ( 93 | // sB64Enc.substr(0, sB64Enc.length - 2 + nMod3) + 94 | // (nMod3 === 2 ? "" : nMod3 === 1 ? "=" : "==") 95 | // ); 96 | // } 97 | 98 | // /* UTF-8 array to JS string and vice versa */ 99 | 100 | // function UTF8ArrToStr(aBytes) { 101 | // let sView = ""; 102 | // let nPart; 103 | // const nLen = aBytes.length; 104 | // for (let nIdx = 0; nIdx < nLen; nIdx++) { 105 | // nPart = aBytes[nIdx]; 106 | // sView += String.fromCodePoint( 107 | // nPart > 251 && nPart < 254 && nIdx + 5 < nLen /* six bytes */ 108 | // ? /* (nPart - 252 << 30) may be not so safe in ECMAScript! So…: */ 109 | // (nPart - 252) * 1073741824 + 110 | // ((aBytes[++nIdx] - 128) << 24) + 111 | // ((aBytes[++nIdx] - 128) << 18) + 112 | // ((aBytes[++nIdx] - 128) << 12) + 113 | // ((aBytes[++nIdx] - 128) << 6) + 114 | // aBytes[++nIdx] - 115 | // 128 116 | // : nPart > 247 && nPart < 252 && nIdx + 4 < nLen /* five bytes */ 117 | // ? ((nPart - 248) << 24) + 118 | // ((aBytes[++nIdx] - 128) << 18) + 119 | // ((aBytes[++nIdx] - 128) << 12) + 120 | // ((aBytes[++nIdx] - 128) << 6) + 121 | // aBytes[++nIdx] - 122 | // 128 123 | // : nPart > 239 && nPart < 248 && nIdx + 3 < nLen /* four bytes */ 124 | // ? ((nPart - 240) << 18) + 125 | // ((aBytes[++nIdx] - 128) << 12) + 126 | // ((aBytes[++nIdx] - 128) << 6) + 127 | // aBytes[++nIdx] - 128 | // 128 129 | // : nPart > 223 && nPart < 240 && nIdx + 2 < nLen /* three bytes */ 130 | // ? ((nPart - 224) << 12) + 131 | // ((aBytes[++nIdx] - 128) << 6) + 132 | // aBytes[++nIdx] - 133 | // 128 134 | // : nPart > 191 && nPart < 224 && nIdx + 1 < nLen /* two bytes */ 135 | // ? ((nPart - 192) << 6) + aBytes[++nIdx] - 128 136 | // : /* nPart < 127 ? */ /* one byte */ 137 | // nPart 138 | // ); 139 | // } 140 | // return sView; 141 | // } 142 | 143 | // function strToUTF8Arr(sDOMStr) { 144 | // let aBytes; 145 | // let nChr; 146 | // const nStrLen = sDOMStr.length; 147 | // let nArrLen = 0; 148 | 149 | // /* mapping… */ 150 | // for (let nMapIdx = 0; nMapIdx < nStrLen; nMapIdx++) { 151 | // nChr = sDOMStr.codePointAt(nMapIdx); 152 | 153 | // if (nChr > 65536) { 154 | // nMapIdx++; 155 | // } 156 | 157 | // nArrLen += 158 | // nChr < 0x80 159 | // ? 1 160 | // : nChr < 0x800 161 | // ? 2 162 | // : nChr < 0x10000 163 | // ? 3 164 | // : nChr < 0x200000 165 | // ? 4 166 | // : nChr < 0x4000000 167 | // ? 5 168 | // : 6; 169 | // } 170 | 171 | // aBytes = new Uint8Array(nArrLen); 172 | 173 | // /* transcription… */ 174 | // let nIdx = 0; 175 | // let nChrIdx = 0; 176 | // while (nIdx < nArrLen) { 177 | // nChr = sDOMStr.codePointAt(nChrIdx); 178 | // if (nChr < 128) { 179 | // /* one byte */ 180 | // aBytes[nIdx++] = nChr; 181 | // } else if (nChr < 0x800) { 182 | // /* two bytes */ 183 | // aBytes[nIdx++] = 192 + (nChr >>> 6); 184 | // aBytes[nIdx++] = 128 + (nChr & 63); 185 | // } else if (nChr < 0x10000) { 186 | // /* three bytes */ 187 | // aBytes[nIdx++] = 224 + (nChr >>> 12); 188 | // aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63); 189 | // aBytes[nIdx++] = 128 + (nChr & 63); 190 | // } else if (nChr < 0x200000) { 191 | // /* four bytes */ 192 | // aBytes[nIdx++] = 240 + (nChr >>> 18); 193 | // aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63); 194 | // aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63); 195 | // aBytes[nIdx++] = 128 + (nChr & 63); 196 | // nChrIdx++; 197 | // } else if (nChr < 0x4000000) { 198 | // /* five bytes */ 199 | // aBytes[nIdx++] = 248 + (nChr >>> 24); 200 | // aBytes[nIdx++] = 128 + ((nChr >>> 18) & 63); 201 | // aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63); 202 | // aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63); 203 | // aBytes[nIdx++] = 128 + (nChr & 63); 204 | // nChrIdx++; 205 | // } /* if (nChr <= 0x7fffffff) */ else { 206 | // /* six bytes */ 207 | // aBytes[nIdx++] = 252 + (nChr >>> 30); 208 | // aBytes[nIdx++] = 128 + ((nChr >>> 24) & 63); 209 | // aBytes[nIdx++] = 128 + ((nChr >>> 18) & 63); 210 | // aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63); 211 | // aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63); 212 | // aBytes[nIdx++] = 128 + (nChr & 63); 213 | // nChrIdx++; 214 | // } 215 | // nChrIdx++; 216 | // } 217 | 218 | // return aBytes; 219 | // } 220 | 221 | 222 | 223 | 224 | // export function encodeBase64(str: string):string { 225 | // const UTF8Array = strToUTF8Arr(str); 226 | // const base64String = base64EncArr(UTF8Array); 227 | // return base64String; 228 | // } 229 | 230 | // export function decodeBase64(str: string):string { 231 | // const UTF8Array = base64DecToArr(str, 1); 232 | // const base64String = UTF8ArrToStr(UTF8Array); 233 | // return base64String; 234 | // } 235 | 236 | // // export function encodeBase64(str: string): string { 237 | // // const x = Buffer.from(str, 'utf8').toString('base64'); 238 | // // console.log(str); 239 | // // console.log('Encoded: ' + x); 240 | // // return x 241 | // // } 242 | 243 | // // export function decodeBase64(str: string): string { 244 | // // const z = Buffer.from(str, 'base64') 245 | // // console.log(`str: ${str}`); 246 | // // console.log(z) 247 | // // const x = z.toString('utf8'); 248 | // // console.log(`Decoded: ${x}`); 249 | // // return x 250 | // // } -------------------------------------------------------------------------------- /src/server/server.ts: -------------------------------------------------------------------------------- 1 | import { Server, WebSocketHandler } from "bun"; 2 | import { BunResponse } from "./response"; 3 | import { 4 | RequestMethod, 5 | Handler, 6 | Middleware, 7 | BunRequest, 8 | SSLOptions, 9 | RequestMapper, 10 | RequestMethodType, 11 | } from "./request"; 12 | import { ExtraHandler, RestSocketHandler } from "./websocket"; 13 | import { Router } from "../router/router"; 14 | import { Chain } from "../utils/chain"; 15 | import { TrieTree } from "./trie-tree"; 16 | // import { encodeBase64 } from "../utils/base64"; 17 | 18 | export function server() { 19 | return BunServer.instance; 20 | } 21 | 22 | class BunServer implements RequestMethod { 23 | // singleton bun server 24 | private static server?: BunServer; 25 | 26 | constructor() { 27 | if (BunServer.server) { 28 | throw new Error( 29 | "DONT use this constructor to create bun server, try Server()" 30 | ); 31 | } 32 | BunServer.server = this; 33 | } 34 | 35 | static get instance() { 36 | return BunServer.server ?? (BunServer.server = new BunServer()); 37 | } 38 | 39 | private readonly requestMap: RequestMapper = {}; 40 | private readonly middlewares: Middleware[] = []; 41 | private readonly errorHandlers: Handler[] = []; 42 | private webSocketHandler: WebSocketHandler | undefined 43 | private webSocketData: (req: BunRequest) => Promise<{ data: DataType}> | undefined 44 | 45 | get(path: string, ...handlers: Handler[]) { 46 | this.delegate(path, "GET", handlers); 47 | } 48 | 49 | put(path: string, ...handlers: Handler[]) { 50 | this.delegate(path, "PUT", handlers); 51 | } 52 | 53 | post(path: string, ...handlers: Handler[]) { 54 | this.delegate(path, "POST", handlers); 55 | } 56 | 57 | patch(path: string, ...handlers: Handler[]) { 58 | this.delegate(path, "PATCH", handlers); 59 | } 60 | 61 | delete(path: string, ...handlers: Handler[]) { 62 | this.delegate(path, "DELETE", handlers); 63 | } 64 | 65 | options(path: string, ...handlers: Handler[]) { 66 | this.delegate(path, "OPTIONS", handlers); 67 | } 68 | 69 | head(path: string, ...handlers: Handler[]) { 70 | this.delegate(path, "HEAD", handlers); 71 | } 72 | 73 | /** 74 | * websocket interface 75 | */ 76 | ws(msgHandler: RestSocketHandler, extra: ExtraHandler = null, data?: (req: BunRequest) => DataType | Promise) { 77 | this.webSocketHandler = { 78 | message: msgHandler, 79 | open: extra?.open, 80 | close: extra?.close, 81 | drain: extra?.drain, 82 | } as WebSocketHandler; 83 | this.webSocketData = data ? async (req) => ({data: await data(req)}) : undefined; 84 | } 85 | 86 | /** 87 | * Add middleware 88 | * @param middleware 89 | */ 90 | use(middleware: Handler): void; 91 | 92 | /** 93 | * Attach router 94 | * @param path 95 | * @param router 96 | */ 97 | use(path: string, router: Router): void; 98 | 99 | /** 100 | * Attch middleware or router or global error handler 101 | * @param arg1 102 | * @param arg2 103 | */ 104 | use(arg1: string | Handler, arg2?: Router) { 105 | // pass router 106 | if (arg2 && typeof arg1 === "string") { 107 | arg2.attach(arg1); 108 | } 109 | // pass middleware or global error handler 110 | else { 111 | if (arg1.length === 3) { 112 | this.middlewares.push(arg1 as Handler); 113 | } else if (arg1.length === 4) { 114 | this.errorHandlers.push(arg1 as Handler); 115 | } 116 | } 117 | } 118 | 119 | router() { 120 | return new Router(this.requestMap, this.middlewares, this.submitToMap); 121 | } 122 | 123 | listen( 124 | port: string | number, 125 | callback?: () => void, 126 | options?: SSLOptions 127 | ): Server { 128 | const baseUrl = "http://localhost:" + port; 129 | callback?.call(null); 130 | return this.openServer(port, baseUrl, options); 131 | } 132 | 133 | private openServer( 134 | port: string | number, 135 | baseUrl: string, 136 | options?: SSLOptions 137 | ): Server { 138 | const that = this; 139 | return Bun.serve({ 140 | port, 141 | reusePort: options?.reusePort, 142 | keyFile: options?.keyFile, 143 | certFile: options?.certFile, 144 | passphrase: options?.passphrase, 145 | caFile: options?.caFile, 146 | dhParamsFile: options?.dhParamsFile, 147 | lowMemoryMode: options?.lowMemoryMode, 148 | development: process.env.SERVER_ENV !== "production", 149 | async fetch(req1: Request, server?: Server) { 150 | const req: BunRequest = await that.bunRequest(req1); 151 | const res = that.responseProxy(); 152 | 153 | //Allow web socket server to function: 154 | if(that.webSocketHandler && server?.upgrade(req1, await that.webSocketData(req))) { 155 | return; 156 | } 157 | 158 | if (req.path.endsWith('/')) { 159 | req.path = req.path.slice(0, req.path.length) 160 | } 161 | 162 | const tree: TrieTree = 163 | that.requestMap[req.method.toLowerCase()]; 164 | 165 | if (!tree) { 166 | throw new Error(`There is no path matches ${req.method}`); 167 | } 168 | 169 | const leaf = tree.get(req.path); 170 | 171 | // fix (issue 4: unhandle route did not throw an error) 172 | if (!leaf.node) { 173 | console.error(`Cannot ${req.method} ${req.path}`); 174 | res.status(404).send(`${req.method} ${req.path} with a 404`) 175 | return res.getResponse() 176 | } 177 | 178 | // append req route params 179 | req.params = leaf.routeParams; 180 | 181 | // middlewares handler 182 | if (that.middlewares.length !== 0) { 183 | const chain = new Chain(req, res, that.middlewares); 184 | await chain.run(); 185 | 186 | if (res.isReady()) { 187 | return res.getResponse(); 188 | } 189 | 190 | if (!chain.isFinish()) { 191 | throw new Error("Please call next() at the end of your middleware"); 192 | } 193 | } 194 | 195 | const handler: Handler[] = leaf.node?.getHandler(); 196 | const middlewares: Handler[] = leaf.node?.getMiddlewares(); 197 | 198 | const chain = new Chain(req, res, middlewares); 199 | await chain.run(); 200 | 201 | if (res.isReady()) { 202 | return res.getResponse(); 203 | } 204 | 205 | if (!chain.isFinish()) { 206 | throw new Error("Please call next() at the end of your middleware"); 207 | } 208 | 209 | // fix (issue 13) : How to make it work with async functions or Promises? 210 | // fix where response data cannot be processed in promise block 211 | const response = handler.apply(that, [req, res]); 212 | if (response instanceof Promise) { 213 | await response; 214 | } 215 | 216 | return res.getResponse(); 217 | }, 218 | websocket: this.webSocketHandler, 219 | error(err: Error) { 220 | const res = that.responseProxy(); 221 | // basically, next here is to ignore the error 222 | const next = () => {}; 223 | that.errorHandlers.forEach((handler) => { 224 | // * no request object pass to error handler 225 | handler.apply(that, [null, res, err, next]); 226 | }); 227 | 228 | if (res.isReady()) { 229 | return res.getResponse(); 230 | } 231 | }, 232 | }); 233 | } 234 | 235 | private async bunRequest(req: Request): Promise { 236 | const { searchParams, pathname } = new URL(req.url); 237 | const newReq: BunRequest = { 238 | method: req.method, 239 | path: pathname, 240 | request: req, 241 | query: {}, 242 | params: {}, 243 | headers: {}, 244 | originalUrl: req.url, 245 | }; 246 | 247 | // append query params 248 | searchParams.forEach((v, k) => { 249 | newReq.query[k] = v; 250 | }); 251 | 252 | // receive request body as string 253 | const bodyStr = await req.text() 254 | try { 255 | newReq.body = JSON.parse(bodyStr) 256 | } catch (err) { 257 | newReq.body = bodyStr 258 | } 259 | req.arrayBuffer; 260 | newReq.blob = req.blob(); 261 | 262 | // append headers 263 | req.headers.forEach((v, k) => { 264 | newReq.headers[k] = v; 265 | }); 266 | 267 | return newReq; 268 | } 269 | 270 | private responseProxy(): BunResponse { 271 | const bunResponse = new BunResponse(); 272 | return new Proxy(bunResponse, { 273 | get(target, prop, receiver) { 274 | if ( 275 | typeof target[prop] === "function" && 276 | (prop === "json" || prop === "send") && 277 | target.isReady() 278 | ) { 279 | throw new Error("You cannot send response twice"); 280 | } else { 281 | return Reflect.get(target, prop, receiver); 282 | } 283 | }, 284 | }); 285 | } 286 | 287 | private delegate(path: string, method: RequestMethodType, handlers: Handler[]) { 288 | let key = path; 289 | 290 | if (key === '/') { 291 | key = '' 292 | } 293 | 294 | if (handlers.length < 1) return; 295 | // Split the array 296 | const middlewares = handlers.slice(0, -1); 297 | const handler = handlers[handlers.length - 1]; 298 | 299 | this.submitToMap(method.toLowerCase(), path, handler, middlewares); 300 | } 301 | 302 | private submitToMap(method: string, path: string, handler: Handler, middlewares: Middleware) { 303 | let targetTree: TrieTree = this.requestMap[method]; 304 | if (!targetTree) { 305 | this.requestMap[method] = new TrieTree(); 306 | targetTree = this.requestMap[method]; 307 | } 308 | const route = { 309 | handler: handler, 310 | middlewareFuncs: middlewares, 311 | } 312 | targetTree.insert(path, route); 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /test/server.test.ts: -------------------------------------------------------------------------------- 1 | import server from '../src'; 2 | import { describe, it, expect, beforeAll, afterAll } from "bun:test"; 3 | import router from './router.test'; 4 | 5 | const app = server(); 6 | 7 | app.get('*', (req, res) => { 8 | res.status(200).send('GET /'); 9 | }); 10 | 11 | app.put('/', (req, res) => { 12 | res.status(200).send('PUT /'); 13 | }); 14 | 15 | app.post('/', (req, res) => { 16 | res.status(200).send('POST /'); 17 | }); 18 | 19 | app.patch('/', (req, res) => { 20 | res.status(200).send('PATCH /'); 21 | }); 22 | 23 | app.delete('/', (req, res) => { 24 | res.status(200).send('DELETE /'); 25 | }); 26 | 27 | app.options('/', (req, res) => { 28 | res.status(200).send('OPTIONS /'); 29 | }); 30 | 31 | app.head('/', (req, res) => { 32 | res.status(200).setHeader('X-Custom-Header', 'Bun is awesome').send('HEAD /'); 33 | }); 34 | 35 | app.use('/route', router); 36 | 37 | app.use((req, res, next) => { 38 | next(); 39 | }); 40 | 41 | app.use((req, res, next) => { 42 | next(); 43 | }); 44 | 45 | app.get('/mid', (req, res, next) => { 46 | res.status(200).send('Middleware /mid'); 47 | }, (req, res) => { }); 48 | 49 | app.get('/mid/nomid', (req, res) => { res.status(200).send('No middleware /mid/nomid')}); 50 | 51 | app.get('/mid/path', (req, res, next) => { 52 | res.status(200).send('Middleware /mid/path'); 53 | }, (req, res) => { }); 54 | 55 | app.get('/mid/:id', (req, res, next) => { 56 | res.status(200).send(`Middleware /mid/${req.params.id}`); 57 | }, (req, res) => { }); 58 | 59 | app.get('/err', (req, res) => { 60 | throw new Error('Err'); 61 | }) 62 | 63 | //add error handler 64 | app.use((req, res, next, err) => { 65 | res.status(500).send('Err /err'); 66 | }); 67 | 68 | const URL_PORT = 5555; 69 | const BASE_URL = `http://localhost:${URL_PORT}`; 70 | 71 | describe('http test', () => { 72 | it('GET', async () => { 73 | const server = app.listen(URL_PORT, () => { 74 | console.log(`App is listening on port ${URL_PORT}`); 75 | }); 76 | try { 77 | const res = await fetch(BASE_URL); 78 | expect(res.status).toBe(200); 79 | expect(await res.text()).toBe('GET /') 80 | } catch (e) { 81 | throw e; 82 | } finally { 83 | server.stop(); 84 | } 85 | }); 86 | it('POST', async () => { 87 | const server = app.listen(URL_PORT, () => { 88 | console.log(`App is listening on port ${URL_PORT}`); 89 | }); 90 | try { 91 | const res = await fetch(BASE_URL, { method: 'POST' }); 92 | expect(res.status).toBe(200); 93 | expect(await res.text()).toBe('POST /') 94 | } catch (e) { 95 | throw e; 96 | } finally { 97 | server.stop(); 98 | } 99 | }); 100 | it('PATCH', async () => { 101 | const server = app.listen(URL_PORT, () => { 102 | console.log(`App is listening on port ${URL_PORT}`); 103 | }); 104 | try { 105 | const res = await fetch(BASE_URL, { method: 'PATCH' }); 106 | expect(res.status).toBe(200); 107 | expect(await res.text()).toBe('PATCH /') 108 | } catch (e) { 109 | throw e; 110 | } finally { 111 | server.stop(); 112 | } 113 | }); 114 | it('PUT', async () => { 115 | const server = app.listen(URL_PORT, () => { 116 | console.log(`App is listening on port ${URL_PORT}`); 117 | }); 118 | try { 119 | const res = await fetch(BASE_URL, { method: 'PUT' }); 120 | expect(res.status).toBe(200); 121 | expect(await res.text()).toBe('PUT /') 122 | } catch (e) { 123 | throw e; 124 | } finally { 125 | server.stop(); 126 | } 127 | }); 128 | it('DELETE', async () => { 129 | const server = app.listen(URL_PORT, () => { 130 | console.log(`App is listening on port ${URL_PORT}`); 131 | }); 132 | try { 133 | const res = await fetch(BASE_URL, { method: 'DELETE' }); 134 | expect(res.status).toBe(200); 135 | expect(await res.text()).toBe('DELETE /') 136 | } catch (e) { 137 | throw e; 138 | } finally { 139 | server.stop(); 140 | } 141 | }); 142 | it('OPTIONS', async () => { 143 | const server = app.listen(URL_PORT, () => { 144 | console.log(`App is listening on port ${URL_PORT}`); 145 | }); 146 | try { 147 | const res = await fetch(BASE_URL, { method: 'OPTIONS' }); 148 | expect(res.status).toBe(200); 149 | //expect(await res.text()).toBe('OPTIONS /') 150 | } catch (e) { 151 | throw e; 152 | } finally { 153 | server.stop(); 154 | } 155 | }); 156 | it('HEAD', async () => { 157 | const server = app.listen(URL_PORT, () => { 158 | console.log(`App is listening on port ${URL_PORT}`); 159 | }); 160 | try { 161 | const res = await fetch(BASE_URL, { method: 'HEAD' }); 162 | expect(res.status).toBe(200); 163 | expect(res.headers.get('X-Custom-Header')).toBe('Bun is awesome') 164 | //expect(await res.text()).toBe('HEAD /') 165 | } catch (e) { 166 | throw e; 167 | } finally { 168 | server.stop(); 169 | } 170 | }); 171 | }) 172 | 173 | describe('router-test', () => { 174 | const url = BASE_URL + '/route'; 175 | it('/route', () => { 176 | it('GET', async () => { 177 | const server = app.listen(URL_PORT, () => { 178 | console.log(`App is listening on port ${URL_PORT}`); 179 | }); 180 | try { 181 | const res = await fetch(url); 182 | expect(res.status).toBe(200); 183 | expect(await res.text()).toBe('GET /route') 184 | } catch (e) { 185 | throw e; 186 | } finally { 187 | server.stop(); 188 | } 189 | }); 190 | it('POST', async () => { 191 | const server = app.listen(URL_PORT, () => { 192 | console.log(`App is listening on port ${URL_PORT}`); 193 | }); 194 | try { 195 | const res = await fetch(url, { method: 'POST' }); 196 | expect(res.status).toBe(200); 197 | expect(await res.text()).toBe('POST /route') 198 | } catch (e) { 199 | throw e; 200 | } finally { 201 | server.stop(); 202 | } 203 | }); 204 | it('PATCH', async () => { 205 | const server = app.listen(URL_PORT, () => { 206 | console.log(`App is listening on port ${URL_PORT}`); 207 | }); 208 | try { 209 | const res = await fetch(url, { method: 'PATCH' }); 210 | expect(res.status).toBe(200); 211 | expect(await res.text()).toBe('PATCH /route') 212 | } catch (e) { 213 | throw e; 214 | } finally { 215 | server.stop(); 216 | } 217 | }); 218 | it('PUT', async () => { 219 | const server = app.listen(URL_PORT, () => { 220 | console.log(`App is listening on port ${URL_PORT}`); 221 | }); 222 | try { 223 | const res = await fetch(url, { method: 'PUT' }); 224 | expect(res.status).toBe(200); 225 | expect(await res.text()).toBe('PUT /route') 226 | } catch (e) { 227 | throw e; 228 | } finally { 229 | server.stop(); 230 | } 231 | }); 232 | it('DELETE', async () => { 233 | const server = app.listen(5555, () => { 234 | console.log(`App is listening on port ${URL_PORT}`); 235 | }); 236 | try { 237 | const res = await fetch(BASE_URL, { method: 'DELETE' }); 238 | expect(res.status).toBe(200); 239 | expect(await res.text()).toBe('DELETE /') 240 | } catch (e) { 241 | throw e; 242 | } finally { 243 | server.stop(); 244 | } 245 | }); 246 | it('OPTIONS', async () => { 247 | const server = app.listen(URL_PORT, () => { 248 | console.log(`App is listening on port ${URL_PORT}`); 249 | }); 250 | try { 251 | const res = await fetch(url, { method: 'OPTIONS' }); 252 | expect(res.status).toBe(200); 253 | expect(await res.text()).toBe('OPTIONS /route') 254 | } catch (e) { 255 | throw e; 256 | } finally { 257 | server.stop(); 258 | } 259 | }); 260 | it('HEAD', async () => { 261 | const server = app.listen(URL_PORT, () => { 262 | console.log(`App is listening on port ${URL_PORT}`); 263 | }); 264 | try { 265 | const res = await fetch(url, { method: 'HEAD' }); 266 | expect(res.status).toBe(200); 267 | expect(await res.text()).toBe('HEAD /route') 268 | } catch (e) { 269 | throw e; 270 | } finally { 271 | server.stop(); 272 | } 273 | }); 274 | }) 275 | }) 276 | 277 | describe('middleware test', () => { 278 | it('middleware /', async () => { 279 | const server = app.listen(URL_PORT, () => { 280 | console.log(`App is listening on port ${URL_PORT}`); 281 | }); 282 | try { 283 | const res = await fetch(BASE_URL + '/mid', { method: 'GET' }); 284 | expect(res.status).toBe(200); 285 | expect(await res.text()).toBe('Middleware /mid') 286 | } catch (e) { 287 | throw e; 288 | } finally { 289 | server.stop(); 290 | } 291 | }) 292 | it('middleware / path', async () => { 293 | const server = app.listen(URL_PORT, () => { 294 | console.log(`App is listening on port ${URL_PORT}`); 295 | }); 296 | try { 297 | const res = await fetch(BASE_URL + '/mid/path', { method: 'GET' }); 298 | expect(res.status).toBe(200); 299 | expect(await res.text()).toBe('Middleware /mid/path') 300 | } catch (e) { 301 | throw e; 302 | } finally { 303 | server.stop(); 304 | } 305 | }) 306 | it('middleware / param', async () => { 307 | const server = app.listen(URL_PORT, () => { 308 | console.log(`App is listening on port ${URL_PORT}`); 309 | }); 310 | try { 311 | const res = await fetch(BASE_URL + '/mid/15', { method: 'GET' }); 312 | expect(res.status).toBe(200); 313 | expect(await res.text()).toBe('Middleware /mid/15') 314 | } catch (e) { 315 | throw e; 316 | } finally { 317 | server.stop(); 318 | } 319 | }) 320 | }) 321 | 322 | // Delete this test because bun test would stop if any error throws, global handlers won't able to stop the throwing 323 | // describe('Error test', () => { 324 | // it('unhandle route', async () => { 325 | // const server = app.listen(5555, () => { 326 | // console.log(`App is listening on port ${URL_PORT}`); 327 | // }) 328 | // 329 | // try { 330 | // const res = await fetch(BASE_URL + '/some_random_route', { method: 'POST' }); 331 | // expect(res.status).toBe(500); 332 | // } catch (e) { 333 | // } finally { 334 | // server.stop(); 335 | // } 336 | // }); 337 | // it('error /err', async () => { 338 | // const server = app.listen(5555, () => { 339 | // console.log(`App is listening on port ${URL_PORT}`); 340 | // }); 341 | // try { 342 | // const res = await fetch(BASE_URL + '/err', { method: 'GET' }); 343 | // expect(res.status).toBe(500); 344 | // expect(await res.text()).toBe('Err /err') 345 | // } catch (e) { 346 | // //throw e; 347 | // } finally { 348 | // server.stop(); 349 | // } 350 | // }); 351 | // }) 352 | --------------------------------------------------------------------------------