An open API service indexing awesome lists of open source software.

https://github.com/edadma/apion

A type-safe HTTP server framework for Scala.js that combines Express-style ergonomics with Scala's powerful type system
https://github.com/edadma/apion

http-server node scala scalajs server

Last synced: 2 months ago
JSON representation

A type-safe HTTP server framework for Scala.js that combines Express-style ergonomics with Scala's powerful type system

Awesome Lists containing this project

README

          


Fluxus Logo

Apion


A lightweight, Express-inspired API server framework for Scala.js that provides a familiar developer experience while leveraging Scala's type safety and immutability.


Release
Maven Central
Release Date
Last Commit
License: ISC
Scala.js: 1.21.0

## Key Features

- Express-like chainable API with type safety
- Pure functions and immutable types
- Unified handler/middleware system
- JWT authentication with role-based access control
- Body parsing for JSON and form data
- Type-safe request/response handling
- Comprehensive error handling
- Static file serving
- Request compression
- CORS and security headers

## Installation

Add to your `build.sbt`:

```scala
libraryDependencies += "io.github.edadma" %%% "apion" % "0.1.0"
```

## Quick Start

```scala 3
import io.github.edadma.apion._
import zio.json._

case class User(name: String, email: String) derives JsonEncoder, JsonDecoder

@main
def run(): Unit =
Server()
.use(LoggingMiddleware())
.use(CorsMiddleware())
.get("/hello", _ => "Hello World!".asText)
.post(
"/users",
_.json[User].flatMap {
case Some(user) => user.asJson(201)
case _ => "Invalid user data".asText(400)
},
)
.listen(3000) { println("Server running at http://localhost:3000") }
```

Test the server using curl:

```bash
# Test the hello endpoint
curl http://localhost:3000/hello

# Create a new user
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'
```

Expected responses:

```
# GET /hello response:
Hello World!

# POST /users response (status 201):
{"name":"Alice","email":"alice@example.com"}
```

## Core Concepts

### Handlers

All request processors (middleware, routes, error handlers) share a unified type:

```scala
type Handler = Request => Future[Result]

sealed trait Result
case class Continue(request: Request) extends Result // Continue processing
case class Complete(response: Response) extends Result // End with response
case class Fail(error: ServerError) extends Result // Propagate error
case object Skip extends Result // Try next route
```

### Middleware

Middleware can modify requests, generate responses, or handle errors:

```scala
// Authentication middleware
val auth = AuthMiddleware(AuthMiddleware.Config(
secretKey = "your-secret-key",
requireAuth = true,
excludePaths = Set("/public"),
audience = Some("your-app"),
issuer = "your-service"
))

// Cookie middleware
val cookies = CookieMiddleware(CookieMiddleware.Options(
secret = Some("cookie-secret"),
parseJSON = true
))

// Security headers
val security = SecurityMiddleware(SecurityMiddleware.Options(
contentSecurityPolicy = true,
frameguard = true,
xssFilter = true
))

server.use(auth).use(cookies).use(security)
```

### Routing

Supports path parameters, nested routes, and route grouping:

```scala
// Path parameters
server.get("/users/:id", request => {
val userId = request.params("id")
getUserById(userId).asJson
})

// Route grouping
val apiRouter = Router()
.use(authMiddleware)
.get("/users", listUsers)
.post("/users", createUser)

server.use("/api", apiRouter)
```

### Request Processing

Access request data with type safety:

```scala
def handler(request: Request): Future[Result] = {
// Access components
val path = request.path
val method = request.method
val headers = request.headers
val params = request.params
val query = request.query

// Get typed body from context
request.json[User].flatMap {
case Some(user) => // Handle user data
case _ => request.failValidation("Invalid body")
}
}
```

### Response Building

Convenient response creation:

```scala
// JSON responses
data.asJson // 200 OK
data.asJson(201) // Created
ErrorResponse("msg").asJson(400) // Bad Request

// Text responses
"Hello".asText // 200 OK
"Created".asText(201) // Created

// Common responses
NotFound // 404 Not Found
BadRequest // 400 Bad Request
ServerError // 500 Internal Error
```

### Error Handling

Type-safe error propagation:

```scala
sealed trait ServerError extends RuntimeException
case class ValidationError(msg: String) extends ServerError
case class AuthError(msg: String) extends ServerError
case class NotFoundError(msg: String) extends ServerError

// In handlers
request.failValidation("Invalid input")
request.failAuth("Unauthorized")
request.failNotFound("Not found")
```

## Additional Features

### Static Files

```scala
server.use(StaticMiddleware("public", StaticMiddleware.Options(
index = true, // Serve index.html for directories
dotfiles = "ignore", // How to handle dotfiles
etag = true, // Enable ETag generation
maxAge = 3600, // Cache max-age in seconds
redirect = true, // Redirect directories to trailing slash
fallthrough = true // Continue to next handler if file not found
)))
```

### Cookie Handling
```scala
server
.use(CookieMiddleware(CookieMiddleware.Options(
secret = Some("cookie-secret"),
parseJSON = true
)))
.get("/set-cookie", request =>
Future.successful(Complete(
Response.text("Cookie set")
.withCookie(
name = "session",
value = "abc123",
maxAge = Some(3600),
httpOnly = true,
secure = true
)
))
)
.get("/read-cookie", request => {
request.cookie("session") match {
case Some(value) => s"Cookie value: $value".asText
case None => "No cookie found".asText(404)
}
})
```

### Compression

```scala
server.use(CompressionMiddleware(CompressionMiddleware.Options(
// Compression filter options
level = 6, // compression level 0-9
threshold = 1024, // minimum size in bytes to compress
memLevel = 8, // memory usage level 1-9
windowBits = 15, // window size 9-15

// Brotli-specific options
brotliQuality = 11, // compression quality 0-11
brotliBlockSize = 4096, // block size 16-24

// Filter options
filter = _ => true, // function to determine if response should be compressed

// Which encodings to support/prefer (in order of preference)
encodings = List("br", "gzip", "deflate")
)))
```

### Body Parsing

```scala
// JSON parsing with type-safe handling
case class User(name: String, email: String) derives JsonEncoder, JsonDecoder

server
.post("/users", request => {
request.json[User].flatMap {
case Some(userData) =>
// Body has been parsed as type User
userData.asJson(201)
case _ =>
"Invalid request body".asText(400)
}
})

// URL-encoded form data parsing
server
.post("/form", request => {
request.form.flatMap {
case Some(formData) =>
// Access form fields
val name = formData.getOrElse("name", "")
formData.asJson
case _ =>
"Invalid form data".asText(400)
}
})
```

## Contributing

1. Fork the repository
2. Create a feature branch
3. Write your changes
4. Add appropriate tests:
- Unit tests for pure functions and utilities
- Integration tests for middleware, request handling chains, and complex features
- Consider both kinds when changes affect multiple areas
5. Verify all tests pass with `sbt test`
6. Push to the branch
7. Create a Pull Request with:
- Description of changes
- Summary of tests added
- Any necessary documentation updates

See `apion/src/test/scala/io/github/edadma/apion` for examples of:
- Unit tests: `JWTTests.scala` shows testing pure JWT functionality
- Integration tests: `AuthIntegrationTests.scala` shows testing middleware behavior in a running server

## License

This project is licensed under the ISC License.