{"id":31932723,"url":"https://github.com/h-dna/expresso","last_synced_at":"2025-10-14T05:21:12.053Z","repository":{"id":265285823,"uuid":"895635726","full_name":"H-DNA/expresso","owner":"H-DNA","description":"Recreational express-like Node.js web framework","archived":false,"fork":false,"pushed_at":"2025-01-28T07:32:29.000Z","size":87,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-06T15:39:16.303Z","etag":null,"topics":["express","framework","nodejs"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/H-DNA.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-11-28T15:23:14.000Z","updated_at":"2025-06-11T17:19:36.000Z","dependencies_parsed_at":"2024-12-31T05:23:44.308Z","dependency_job_id":"e7e279b7-38c5-4a80-8aeb-f776ab653753","html_url":"https://github.com/H-DNA/expresso","commit_stats":null,"previous_names":["huy-dna/eekpress","huy-dna/expresso","h-dna/expresso"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/H-DNA/expresso","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/H-DNA%2Fexpresso","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/H-DNA%2Fexpresso/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/H-DNA%2Fexpresso/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/H-DNA%2Fexpresso/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/H-DNA","download_url":"https://codeload.github.com/H-DNA/expresso/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/H-DNA%2Fexpresso/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279018015,"owners_count":26086237,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-14T02:00:06.444Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["express","framework","nodejs"],"created_at":"2025-10-14T05:21:10.809Z","updated_at":"2025-10-14T05:21:12.038Z","avatar_url":"https://github.com/H-DNA.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# expresso\n\nRecreate a Node.js (actually Deno) web framework. The API mirrors (a subset of) [expressjs](https://expressjs.com/).\n\n## Rationale\n\n- Personal engineering skill issues:\n  - Sloppy abstractions.\n    - Wrong abstractions.\n    - Unnecessary abstraction or high-cognitive-load abstraction.\n    - Abstractions that don't compose.\n  - Unnecessarily complex code.\n- The `expressjs` API to me is very elegant:\n  - Simple/Straightforward.\n  - Streamline:\n    ```ts\n    res.status(200).send(\"This is the response\");\n    ```\n  - Composable: You can create complex handlers and register it with `express()`. The handler can be a simple function or a complex `Router`, which is itself a set of functions or `Router`s.\n    ```ts\n    const cRouter = express.Router();\n    cRouter.use('/child', ...);\n\n    const pRouter = express.Router();\n    pRouter.use('/parent', cRouter);\n\n    const app = express();\n    app.use('/app', pRouter);\n    ```\n    \n    After reading the classic SICP book ([my notes](https://github.com/Huy-DNA/sicp)), this property is called `closure` - `expressjs` allows the combination of handlers into a complex handler and the resulting handler can itself be combined further with other handlers.\n- I want to play with Node's HTTP API a bit.\n\n## Disclaimer\n\nI don't intend to make this `express`-compatible. I can go extra length to do this, which will unlock the following things:\n- Readily available test cases from `express` itself.\n- The diverse plugin ecosystem of `express`.\n\nThis project is nothing but a toy for self-orientation.\n\n## Why Node's HTTP API is not enough\n\nAs far as I know, the HTTP API in Node handles HTTP at mostly the connection level:\n  - It can accept and serve concurrent connections well. The serving logic is left to the programmer.\n  - It allows us to receive requests and send appropriate responses.\n  - It provides us a low-level mechanism to extract request headers, cookies and body.\n\nThis is still lackluster (understandable) to be an HTTP-compliant HTTP server \u0026 a web framework.\n\n  - In terms of an HTTP-compliant HTTP server, we need:\n    - Content negotiation.\n    - Conditional-GET handling.\n    - Range requests.\n    - etc.\n\n    These are mostly left to the programmer in the Node's HTTP API \u0026 even in `express` to allow for more flexibility. However, `express` does provide ways to handle these easier.\n\n  - In terms of a Web framework:\n    - The HTTP API is primitive:\n      - HTTP request body is streamed incrementally and we need to put together the chunks to get the full HTTP body.\n      - HTTP cookies need to be manually parsed.\n      - etc.\n    - Some common functionality is missing:\n      - Specifying routes \u0026 handlers.\n      - Specifying middleware.\n      - HTTP request body parsing based on its `Content-Type` header.\n      - Quickly build responses: send cookies, serve static files, render HTML.\n\n## Philosophy\n\nKeep it simple. Just write the code first \u0026 abstract when needed *or* may abstract in advance but only minimally.\n\n## What is implemented?\n\nI intend to cover the breadth but not much depth:\n\n- Easily extract headers, cookies, querystring, body from a request.\n- Easily set headers, cookie, body on a response.\n- Register a handler for a route. `Router`s are not supported.\n- Serve static files.\n- Support templating engines.\n\n## Experience\n\n- Path manipulation is tricky. Common operations I perform on a path:\n  - Equality check:\n    - Handle paths that can come in with or without the `/`.\n    - Handle `..` and `.`\n  - Ancestor/Descendant directory check:\n    - Either the ancestor/descendant path can come in with or without `/`.\n\n  -\u003e May worth creating a `Path` abstraction to:\n    - Normalize paths: Remove `..`, `.` and standardize whether to include trailing `/`.\n    - Perform equality check.\n    - Perform ancestor/descendant check.\n- Serving static files takes some considerations and I may miss something:\n  - Security issues:\n    - Scope serving requests to some folder only: Be careful of relative paths, etc. This can lead to arbitrary files in the filesystem being sent.\n    - Consider dotfiles - `static-serve` ignores these by default.\n    - Avoid following symlinks (or just following but avoid allowing users to upload symlink). I just follow symlinks in this project.\n  - Range requests.\n\n- Always read raw data into `Buffer` instead of `string`.\n\n## API reference\n\nI don't want to create noise on jsr for this hobby project so if you wanna test this, please replace `'expresso'` with the path you clone it into in your project.\n\n```ts\nimport expresso from 'expresso';\n```\n\n### App\n\n```ts\nimport expresso from 'expresso';\n\nconst app = expresso();\n```\n\n#### `App.use([path]: string | string[], callback: Handler): App`\n\nLike `expressjs`'s `app.use`.\n\n- `path`: The path for which the middleware function is invoked; can be any of:\n  - A string representing a path.\n  - A path pattern string using an extension of JS's regex notation (this differs from `express`).\n  - An array of combinations of any of the above.\n- `callback`: A function of type `(req: Request, res: Response, next: (void) =\u003e unknown) =\u003e unknown`.\n\nThe path pattern strings can also contain the `:param` pattern, which is equivalent to: `(?\u003cparam\u003e[^/]+)`.\n\nWith `path` omitted, `'.*'` is assumed and `callback` is invoked for all `path`.\n\n#### `App.METHOD([path]: string | string[], callback: Handler): App`\n\nLike `expressjs`'s `app.METHOD`.\n\n`METHOD` is one of the HTTP methods or `all`.\n\n#### `App.listen(port)`\n\nStart the HTTP and listening on port `port`.\n\n#### `App.close()`\n\nClose the HTTP server.\n\n### Request\n\n#### `Request.body`\n\nThe request's body. By default, this is `undefined` - you need to use the `body-parser` plugin.\n\n```ts\nimport { bodyParser } from 'expresso'\napp.use([path], bodyParser.raw);  // `req.body` will return the raw content of the request's body.\napp.use([path], bodyParser.json);  // `req.body` will return the content as json of the request's body.\napp.use([path], bodyParser.urlencoded); // `req.body` will return the content of the request's body as urlencoded string.\n```\n\n#### `Request.cookies`\n\nThe request's cookies, which is an key-value object mapping cookie's name to the cookie's value.\n\n#### `Request.host`\n\nThe value of the request's `Host` header. If the `Host` header is missing, this field is set to `\"\"`.\n\n#### `Request.method`\n\nThe method of the request.\n\n#### `Request.originalUrl`\n\nThe full URL of the request.\n\n#### `Request.path`\n\nThe requested path of the request, omitting the query string.\n\n```ts\n\"/path?qs=q\" // -\u003e \"/path\"\n\"/path\" // -\u003e \"/path\"\n```\n\n#### `Request.params`\n\nThe path parameters of the matched route.\n\n```ts\n\"/path/:id\" // -\u003e { id: \"...\" }\n```\n\n#### `Request.query`\n\nThe query string of the request, parsed using the `qs` package.\n\n```ts\n\"/path?qs=q\" // { qs: \"q\" }\n\"/path?qs[0]=q1\u0026qs[1]=q2\" // { qs: [ \"q1\", \"q2\" ] }\n```\n\n#### `Request.ip`\n\nThe IP address of the client.\n\n### `Response`\n\n#### `Response.append(field: string, values: string | string[]): Response`\n\nSet the header `field` to a header value or an array of header values. If the header already exists, the header value is appended instead of being overridden.\n\n#### `Response.get(field: string): string | string[] | undefined`\n\nGet the value of the header `field`.\n\n#### `Response.set(field, value: string): Response`\n\nSet the header `field` to `value`.\n\n#### `Response.cookie(name: string, value: string, options: object): Response`\n\nSet a cookie `name` to `value` along with some options. The `options` object has the following optional fields:\n\n- `domain?: string`: The `Domain` property of the cookie.\n- `encode?: (_: string) =\u003e string`: Encode the cookie's value.\n- `expires?: Date | 0`: The `Expires` property of the cookie.\n- `httpOnly?: boolean`: The `HttpOnly` property of the cookie.\n- `maxAge?: number`: The `Max-Age` property of the cookie.\n- `path?: string`: The `Path` property of the cookie.\n- `secure?: boolean`: The `Secure` property of the cookie.\n- `sameSite?: \"None\" | \"Lax\" | \"Strict\"`: The `Same-Site` property of the cookie.\n\n#### `Response.status(code: number): Response`\n\nSet the response's status code.\n\n#### `Response.sendStatus(code: number): Response`\n\nSet the response's status code and the body also to `code`.\n\n#### `Response.send(data: Buffer | string | JsonConvertible): Response`\n\n- If `data` is a `Buffer`:\n  - Set the response's body to `data`.\n  - Set the `Content-Type` header to `application/octet-stream`.\n  - Set the `Content-Length` header to `data`'s length.\n- If `data` is a `String`:\n  - Set the response's body to `data`.\n  - Set the `Content-Type` header to `text/html`.\n  - Set the `Content-Length` header to `data`'s length.\n- If `data` is a `JsonConvertible` value:\n  - Set the response's body to `data`.\n  - Set the `Content-Type` header to `application/json`.\n  - Set the `Content-Length` header to `data`'s length.\n\n#### `Response.raw(data: Buffer | string): Response`\n\nLike `Response.send` but do not set `Content-Type`.\n\n#### `Response.json(data: JsonConvertible): Response`\n\nSet the response's body to `data` and set the `Content-Type` header to `application/json`.\n\n#### `Response.location(path: string): Response`\n\nSet the `Location` header to `path`.\n\nIf `path` is `\"back\"`, set `Location` to the corresponding `Request`'s `Referer` header.\n\n#### `Response.redirect([status]: number, path: string): Response`\n\nSet the status code to `status` (by default 302) and set the `Location` header to `path`.\n\n#### `Response.headersSent(): object`\n\nReturn the headers sent on this response.\n\n#### `Response.vary(value: string): Response`\n\nAppend `value` to the `Vary` header.\n\n#### `Reponse.type(value: string): Response`\n\nSet the `Content-Type` header to `value`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fh-dna%2Fexpresso","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fh-dna%2Fexpresso","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fh-dna%2Fexpresso/lists"}