{"id":26140807,"url":"https://github.com/cakekindel/purescript-axon","last_synced_at":"2026-03-09T12:33:59.568Z","repository":{"id":280198397,"uuid":"941269399","full_name":"cakekindel/purescript-axon","owner":"cakekindel","description":"HTTP server library inspired by axum; best-in-class expressiveness \u0026 backend-agnostic.","archived":false,"fork":false,"pushed_at":"2025-05-21T18:46:00.000Z","size":196,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-05-21T18:48:39.679Z","etag":null,"topics":["api","http","http-server","purescript","rest","server"],"latest_commit_sha":null,"homepage":"","language":"PureScript","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/cakekindel.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","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,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-03-01T22:11:23.000Z","updated_at":"2025-05-21T18:46:04.000Z","dependencies_parsed_at":"2025-05-01T19:26:46.309Z","dependency_job_id":"e0a4e238-b2e1-4c5b-9650-468e85f21af5","html_url":"https://github.com/cakekindel/purescript-axon","commit_stats":null,"previous_names":["cakekindel/purescript-axon"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/cakekindel/purescript-axon","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cakekindel%2Fpurescript-axon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cakekindel%2Fpurescript-axon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cakekindel%2Fpurescript-axon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cakekindel%2Fpurescript-axon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cakekindel","download_url":"https://codeload.github.com/cakekindel/purescript-axon/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cakekindel%2Fpurescript-axon/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30295221,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-09T11:12:22.024Z","status":"ssl_error","status_checked_at":"2026-03-09T11:10:54.577Z","response_time":61,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["api","http","http-server","purescript","rest","server"],"created_at":"2025-03-11T02:57:31.811Z","updated_at":"2026-03-09T12:33:59.538Z","avatar_url":"https://github.com/cakekindel.png","language":"PureScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# axon\n\n**WIP**\n\nHTTP server library inspired by [`axum`](https://docs.rs/latest/axum), allowing best-in-class\nexpressive routing.\n\nThe main difference between this server library compared to others (eg. the wonderful [`httpurple`](https://github.com/sigma-andex/purescript-httpurple))\nis the philosophy around **routing**.\n\nThe core abstraction is a [`Handler`](./src/Axon.Request.Handler.purs); any function with the type `[...Request parts] -\u003e m Response`.\n\nThis allows each REST action to correspond to a single function, which declares its requirements in its type signature.\nThis allows for a highly refactorable \u0026 composable application, as opposed to a hierarchical routing approach like `routing-duplex`.\n\nFor example, an endpoint `GET /persons/:id/address` would be modeled as:\n\n```purs\ngetPersonAddress :: Get -\u003e Path (\"persons\" / Int / \"address\") Int -\u003e Aff Response\ngetPersonAddress _ (Path id) = ...\n```\n\n`POST /persons` accepting a json body:\n\n```purs\ntype Person = { firstName :: String, lastName :: String, age :: Maybe Int }\n\npostPerson :: Post -\u003e Path \"persons\" Unit -\u003e ContentType Json -\u003e Json Person -\u003e Aff Response\npostPerson _ _ _ person = ...\n```\n\nThen these can be rolled up into a `/persons` resource with `Handler.or`:\n\n```purs\npersons :: Handler Aff Response\npersons = getPerson `Handler.or` postPerson `Handler.or` deletePerson `Handler.or` getPersonAddress ...\n```\n\nThen run with:\n\n```purs\nAxon.serveNode {port: 10000, hostname: \"0.0.0.0\"} persons\n```\n\n## Example\n\nThis example implements this REST interface in 36LoC:\n\n- `GET /cheeses` - Lists all cheeses (strings) known to server\n- `POST /cheeses` - Add a cheese to the cheese list\n- `DELETE /cheeses/:cheese` - Remove a cheese from the cheese list\n\n```purs\nmodule Main where\n\nimport Prelude\n\nimport Axon as Axon\nimport Axon.Request.Handler as Handler\nimport Axon.Request.Parts.Class (Delete, Get, Path(..), Post)\nimport Axon.Request.Parts.Path (type (/))\nimport Axon.Response (Response)\nimport Axon.Response.Construct (Json(..), toResponse)\nimport Axon.Response.Status as Status\nimport Data.Filterable (filter)\nimport Data.Foldable (elem)\nimport Data.Tuple.Nested ((/\\))\nimport Effect (Effect)\nimport Effect.Aff (Aff, launchAff_, joinFiber)\nimport Effect.Aff as Aff\nimport Effect.Class (liftEffect)\nimport Effect.Ref (Ref)\nimport Effect.Ref as Ref\n\nmain :: Effect Unit\nmain = launchAff_ do\n  cheeses :: Ref (Array String) \u003c- liftEffect $ Ref.new\n    [ \"cheddar\", \"swiss\", \"gouda\" ]\n\n  let\n    getCheeses :: Get -\u003e Path \"cheeses\" _ -\u003e Aff Response\n    getCheeses _ _ = liftEffect do\n      cheeses' \u003c- Ref.read cheeses\n      toResponse $ Status.ok /\\ Json cheeses'\n\n    deleteCheese :: Delete -\u003e Path (\"cheeses\" / String) _ -\u003e Aff Response\n    deleteCheese _ (Path id) = liftEffect do\n      cheeses' \u003c- Ref.read cheeses\n      if not $ elem id cheeses' then\n        toResponse Status.notFound\n      else\n        Ref.modify_ (filter (_ /= id)) cheeses\n        *\u003e toResponse Status.accepted\n\n    postCheese :: Post -\u003e Path \"cheeses\" _ -\u003e String -\u003e Aff Response\n    postCheese _ _ cheese =\n      let\n        tryInsert as\n          | elem cheese as = { state: as, value: false }\n          | otherwise = { state: as \u003c\u003e [ cheese ], value: true }\n      in\n        liftEffect\n          $ Ref.modify' tryInsert cheeses\n          \u003e\u003e= if _ then toResponse Status.accepted else toResponse Status.conflict\n\n  handle \u003c-\n    Axon.serveBun\n      { port: 8080, hostname: \"localhost\" }\n      (getCheeses `Handler.or` postCheese `Handler.or` deleteCheese)\n\n  joinFiber handle.join\n```\n\n## Request Handlers\n\nRequest handler functions have any number of parameters that are `RequestParts` and return an `Aff Response` (or any `MonadAff`).\n\n\u003cdetails\u003e\n\u003csummary\u003e\n\n`RequestParts`\n\n\u003c/summary\u003e\n\n- `Request`\n  - Always succeeds; provides the entire request\n- **Combinators**\n  - `Unit`\n    - Always succeeds\n  - `a /\\ b`\n    - Tuple of `a` and `b`, where `a` and `b` are `RequestParts`.\n  - `Maybe a`\n    - `a` must be `RequestParts`. If `a` can't be extracted, the handler will still succeed and this will be `Nothing`. If `a` was extracted, it's wrapped in `Just`.\n  - `Either a b`\n    - `a` and `b` must be `RequestParts`. Succeeds if either `a` or `b` succeeds (preferring `a`). Fails if both fail.\n- **Body**\n  - `String`\n    - succeeds when request has a non-empty body that is valid UTF-8\n  - `Json a`\n    - succeeds when request has a `String` body (see above) that can be parsed into `a` using `DecodeJson`.\n  - `Buffer`\n    - succeeds when request has a nonempty body.\n  - `Stream`\n    - succeeds when request has a nonempty body.\n- **Headers**\n  - `Header a`\n    - `a` must be `TypedHeader` from `Axon.Header.Typed`. Allows statically (ex. `ContentType Type.MIME.Json`) or dynamically (ex. `ContentType String`) matching request headers.\n  - `HeaderMap`\n    - All headers provided in the request\n- **Path**\n  - `Path a c`\n    - Statically match the path of the request, and extract parameters. See `Axon.Request.Parts.Path`. (TODO: this feels too magical, maybe follow axum's prior art of baking paths into the router declaration?)\n- **Method** - `Get` - `Post` - `Put` - `Patch` - `Delete` - `Options` - `Connect` - `Trace`\n\u003c/details\u003e\n\nSimilarly to the structural extraction of request parts; handlers can use `Axon.Response.Construct.ToResponse` for easily constructing responses.\n\n\u003cdetails\u003e\n\u003csummary\u003e\n\n`ToResponse`\n\n\u003c/summary\u003e\n\n- **Combinators**\n  - `Status /\\ a`\n    - Special case to make sure any `Status` in a tuple will take priority over any default statuses within. TODO: This case (overlapping with `a /\\ b` requires the class to be \"sealed\" in an instance chain. Want a clean way around this so consumers can implement `ToResponse`.)\n  - `a /\\ b`\n    - Merges `toResponse a` and `toResponse b`, using `b` on conflicts\n- **Status**\n  - `Axon.Response.Status.Status`\n- **Body**\n  - `Axon.Response.Body.Body`\n  - `String`\n  - `Node.Buffer.Buffer`\n  - `Node.Stream.Readable a` (for all `a`)\n  - `Axon.Response.Construct.Json a`\n    - `a` must be `EncodeJson`. This will set the body to `a` stringified, and set `Content-Type` to `application/json`.\n- **Headers**\n  - `ToResponse` is implemented for all implementors of `TypedHeader`\n  - TODO: `Map String String`\n  \u003c/details\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcakekindel%2Fpurescript-axon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcakekindel%2Fpurescript-axon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcakekindel%2Fpurescript-axon/lists"}