{"id":23820038,"url":"https://github.com/edadma/apion","last_synced_at":"2026-04-18T06:14:51.307Z","repository":{"id":270122542,"uuid":"909396800","full_name":"edadma/apion","owner":"edadma","description":"A type-safe HTTP server framework for Scala.js that combines Express-style ergonomics with Scala's powerful type system","archived":false,"fork":false,"pushed_at":"2026-04-18T04:46:20.000Z","size":890,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"stable","last_synced_at":"2026-04-18T05:36:42.114Z","etag":null,"topics":["http-server","node","scala","scalajs","server"],"latest_commit_sha":null,"homepage":"https://edadma.github.io/apion/","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/edadma.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-12-28T15:25:45.000Z","updated_at":"2026-04-18T04:45:29.000Z","dependencies_parsed_at":"2024-12-28T16:28:43.306Z","dependency_job_id":"8d45f9c0-5f87-4ce2-8f6b-9971fa944fc9","html_url":"https://github.com/edadma/apion","commit_stats":null,"previous_names":["edadma/apion"],"tags_count":10,"template":false,"template_full_name":"edadma/scalajs-template","purl":"pkg:github/edadma/apion","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edadma%2Fapion","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edadma%2Fapion/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edadma%2Fapion/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edadma%2Fapion/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/edadma","download_url":"https://codeload.github.com/edadma/apion/tar.gz/refs/heads/stable","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edadma%2Fapion/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31958677,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T00:39:45.007Z","status":"online","status_checked_at":"2026-04-18T02:00:07.018Z","response_time":103,"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":["http-server","node","scala","scalajs","server"],"created_at":"2025-01-02T07:17:39.592Z","updated_at":"2026-04-18T06:14:51.301Z","avatar_url":"https://github.com/edadma.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"public/apion.png\" width=\"200\" alt=\"Fluxus Logo\"\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003eApion\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  A lightweight, Express-inspired API server framework for Scala.js that provides a familiar developer experience while leveraging Scala's type safety and immutability.\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/edadma/apion/releases\"\u003e\u003cimg src=\"https://img.shields.io/github/v/release/edadma/apion?include_prereleases\u0026sort=semver\" alt=\"Release\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://central.sonatype.com/artifact/io.github.edadma/apion_sjs1_3\"\u003e\u003cimg src=\"https://img.shields.io/maven-central/v/io.github.edadma/apion_sjs1_3\" alt=\"Maven Central\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/edadma/apion/releases\"\u003e\u003cimg src=\"https://img.shields.io/github/release-date-pre/edadma/apion\" alt=\"Release Date\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/edadma/apion/commits\"\u003e\u003cimg src=\"https://img.shields.io/github/last-commit/edadma/apion\" alt=\"Last Commit\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://opensource.org/licenses/ISC\"\u003e\u003cimg src=\"https://img.shields.io/badge/license-ISC-blue.svg\" alt=\"License: ISC\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.scala-js.org\"\u003e\u003cimg src=\"https://img.shields.io/badge/scala.js-1.21.0-blue.svg\" alt=\"Scala.js: 1.21.0\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n## Key Features\n\n- Express-like chainable API with type safety\n- Pure functions and immutable types\n- Unified handler/middleware system\n- JWT authentication with role-based access control\n- Body parsing for JSON and form data\n- Type-safe request/response handling\n- Comprehensive error handling\n- Static file serving\n- Request compression\n- CORS and security headers\n\n## Installation\n\nAdd to your `build.sbt`:\n\n```scala\nlibraryDependencies += \"io.github.edadma\" %%% \"apion\" % \"0.1.0\"\n```\n\n## Quick Start\n\n```scala 3\nimport io.github.edadma.apion._\nimport zio.json._\n\ncase class User(name: String, email: String) derives JsonEncoder, JsonDecoder\n\n@main\ndef run(): Unit =\n  Server()\n    .use(LoggingMiddleware())\n    .use(CorsMiddleware())\n    .get(\"/hello\", _ =\u003e \"Hello World!\".asText)\n    .post(\n      \"/users\",\n       _.json[User].flatMap {\n          case Some(user) =\u003e user.asJson(201)\n          case _          =\u003e \"Invalid user data\".asText(400)\n       },\n    )\n    .listen(3000) { println(\"Server running at http://localhost:3000\") }\n```\n\nTest the server using curl:\n\n```bash\n# Test the hello endpoint\ncurl http://localhost:3000/hello\n\n# Create a new user\ncurl -X POST http://localhost:3000/users \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\": \"Alice\", \"email\": \"alice@example.com\"}'\n```\n\nExpected responses:\n\n```\n# GET /hello response:\nHello World!\n\n# POST /users response (status 201):\n{\"name\":\"Alice\",\"email\":\"alice@example.com\"}\n```\n\n## Core Concepts\n\n### Handlers\n\nAll request processors (middleware, routes, error handlers) share a unified type:\n\n```scala\ntype Handler = Request =\u003e Future[Result]\n\nsealed trait Result\ncase class Continue(request: Request) extends Result   // Continue processing\ncase class Complete(response: Response) extends Result // End with response\ncase class Fail(error: ServerError) extends Result     // Propagate error  \ncase object Skip extends Result                        // Try next route\n```\n\n### Middleware\n\nMiddleware can modify requests, generate responses, or handle errors:\n\n```scala\n// Authentication middleware\nval auth = AuthMiddleware(AuthMiddleware.Config(\n  secretKey = \"your-secret-key\",\n  requireAuth = true,\n  excludePaths = Set(\"/public\"),\n  audience = Some(\"your-app\"),\n  issuer = \"your-service\"\n))\n\n// Cookie middleware\nval cookies = CookieMiddleware(CookieMiddleware.Options(\n  secret = Some(\"cookie-secret\"),\n  parseJSON = true\n))\n\n// Security headers\nval security = SecurityMiddleware(SecurityMiddleware.Options(\n  contentSecurityPolicy = true,\n  frameguard = true,\n  xssFilter = true\n))\n\nserver.use(auth).use(cookies).use(security)\n```\n\n### Routing\n\nSupports path parameters, nested routes, and route grouping:\n\n```scala\n// Path parameters\nserver.get(\"/users/:id\", request =\u003e {\n  val userId = request.params(\"id\")\n  getUserById(userId).asJson\n})\n\n// Route grouping\nval apiRouter = Router()\n  .use(authMiddleware)\n  .get(\"/users\", listUsers)\n  .post(\"/users\", createUser)\n\nserver.use(\"/api\", apiRouter)\n```\n\n### Request Processing\n\nAccess request data with type safety:\n\n```scala\ndef handler(request: Request): Future[Result] = {\n  // Access components\n  val path = request.path\n  val method = request.method\n  val headers = request.headers\n  val params = request.params\n  val query = request.query\n  \n  // Get typed body from context\n  request.json[User].flatMap {\n    case Some(user) =\u003e // Handle user data\n    case _ =\u003e request.failValidation(\"Invalid body\")\n  }\n}\n```\n\n### Response Building\n\nConvenient response creation:\n\n```scala\n// JSON responses\ndata.asJson                      // 200 OK\ndata.asJson(201)                 // Created\nErrorResponse(\"msg\").asJson(400) // Bad Request\n\n// Text responses\n\"Hello\".asText                   // 200 OK\n\"Created\".asText(201)            // Created\n\n// Common responses\nNotFound                         // 404 Not Found\nBadRequest                       // 400 Bad Request\nServerError                      // 500 Internal Error\n```\n\n### Error Handling\n\nType-safe error propagation:\n\n```scala\nsealed trait ServerError extends RuntimeException\ncase class ValidationError(msg: String) extends ServerError\ncase class AuthError(msg: String) extends ServerError\ncase class NotFoundError(msg: String) extends ServerError\n\n// In handlers\nrequest.failValidation(\"Invalid input\")\nrequest.failAuth(\"Unauthorized\")\nrequest.failNotFound(\"Not found\")\n```\n\n## Additional Features\n\n### Static Files\n\n```scala\nserver.use(StaticMiddleware(\"public\", StaticMiddleware.Options(\n  index = true,          // Serve index.html for directories\n  dotfiles = \"ignore\",   // How to handle dotfiles\n  etag = true,           // Enable ETag generation\n  maxAge = 3600,         // Cache max-age in seconds\n  redirect = true,       // Redirect directories to trailing slash\n  fallthrough = true     // Continue to next handler if file not found\n)))\n```\n\n### Cookie Handling\n```scala\nserver\n  .use(CookieMiddleware(CookieMiddleware.Options(\n    secret = Some(\"cookie-secret\"),\n    parseJSON = true\n  )))\n  .get(\"/set-cookie\", request =\u003e \n    Future.successful(Complete(\n      Response.text(\"Cookie set\")\n        .withCookie(\n          name = \"session\",\n          value = \"abc123\",\n          maxAge = Some(3600),\n          httpOnly = true,\n          secure = true\n        )\n    ))\n  )\n  .get(\"/read-cookie\", request =\u003e {\n    request.cookie(\"session\") match {\n      case Some(value) =\u003e s\"Cookie value: $value\".asText\n      case None =\u003e \"No cookie found\".asText(404)\n    }\n  })\n```\n\n### Compression\n\n```scala\nserver.use(CompressionMiddleware(CompressionMiddleware.Options(\n  // Compression filter options\n  level = 6,          // compression level 0-9\n  threshold = 1024,   // minimum size in bytes to compress\n  memLevel = 8,       // memory usage level 1-9\n  windowBits = 15,    // window size 9-15\n\n  // Brotli-specific options\n  brotliQuality = 11,      // compression quality 0-11\n  brotliBlockSize = 4096,  // block size 16-24\n\n  // Filter options\n  filter = _ =\u003e true,  // function to determine if response should be compressed\n\n  // Which encodings to support/prefer (in order of preference)\n  encodings = List(\"br\", \"gzip\", \"deflate\")\n)))\n```\n\n### Body Parsing\n\n```scala\n// JSON parsing with type-safe handling\ncase class User(name: String, email: String) derives JsonEncoder, JsonDecoder\n\nserver\n  .post(\"/users\", request =\u003e {\n    request.json[User].flatMap {\n      case Some(userData) =\u003e\n        // Body has been parsed as type User\n        userData.asJson(201)\n      case _ =\u003e\n        \"Invalid request body\".asText(400)\n    }\n  })\n\n// URL-encoded form data parsing\nserver\n  .post(\"/form\", request =\u003e {\n    request.form.flatMap {\n      case Some(formData) =\u003e\n        // Access form fields\n        val name = formData.getOrElse(\"name\", \"\")\n        formData.asJson\n      case _ =\u003e\n        \"Invalid form data\".asText(400)\n    }\n  })\n```\n\n## Contributing\n\n1. Fork the repository\n2. Create a feature branch\n3. Write your changes\n4. Add appropriate tests:\n    - Unit tests for pure functions and utilities\n    - Integration tests for middleware, request handling chains, and complex features\n    - Consider both kinds when changes affect multiple areas\n5. Verify all tests pass with `sbt test`\n6. Push to the branch\n7. Create a Pull Request with:\n    - Description of changes\n    - Summary of tests added\n    - Any necessary documentation updates\n\nSee `apion/src/test/scala/io/github/edadma/apion` for examples of:\n- Unit tests: `JWTTests.scala` shows testing pure JWT functionality\n- Integration tests: `AuthIntegrationTests.scala` shows testing middleware behavior in a running server\n\n## License\n\nThis project is licensed under the ISC License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedadma%2Fapion","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fedadma%2Fapion","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedadma%2Fapion/lists"}