API Reference
Welcome to the Apion API documentation. This guide provides comprehensive information about Apion’s components, patterns, and design principles.
Design Philosophy
Apion is built on three core principles:
- Type Safety: Leverage Scala’s type system to prevent errors at compile time
- Immutability: Use immutable data structures for predictable behavior
- Composability: Build complex applications from simple, reusable components
Core Architecture
Request → Router → Middleware Chain → Handler → Response
↓
Error Handler
Request/Response Pipeline
- Router receives request and matches route
- Middleware processes request in order
- Handler generates response
- Response flows back through middleware
- Finalizers transform final response
Core Components
Server & Router
The entry point for your application:
val server = Server()
.use(globalMiddleware)
.get("/users", listUsers)
.post("/users", createUser)
// Start server
server.listen(3000) {
println("Server running")
}
Key features:
- Chainable API
- Type-safe route handling
- Middleware support
- Error propagation
Request Processing
Type-safe request handling:
def handler(request: Request): Future[Result] = {
// Access request data safely
val userId = request.params("id")
val auth = request.context.get("auth")
// Type-safe body parsing
request.json[User].flatMap {
case Some(user) => processUser(user)
case None => request.failValidation("Invalid user data")
}
}
Components:
- Request - Request data and operations
- Response - Response building
- Error Handling - Error types and handling
Middleware System
Middleware Types
- Request Transformation
request => Future.successful(Continue( request.copy(context = request.context + ("key" -> "value")) ))
- Response Generation
request => "Response".asText
- Error Handling
request => request.failAuth("Unauthorized")
- Response Transformation
val finalizer: Finalizer = (req, res) => Future.successful(res.withHeader("X-Custom", "value"))
Built-in Middleware
Security:
- Authentication - JWT-based auth
- Security Headers - Security best practices
- CORS - Cross-origin resource sharing
- Rate Limiting - Request throttling
Content:
- Static Files - File serving
- Compression - Response compression
Utilities:
Type Safety Features
Route Parameters
// Type-safe parameter extraction
server.get("/users/:id", request => {
val userId: String = request.params("id")
getUserById(userId).asJson
})
JSON Handling
case class User(name: String, email: String) derives JsonEncoder, JsonDecoder
// Compile-time JSON validation
request.json[User].flatMap {
case Some(user) => user.asJson(201)
case None => request.failValidation("Invalid JSON")
}
Error Propagation
sealed trait ServerError
case class ValidationError(msg: String) extends ServerError
case class AuthError(msg: String) extends ServerError
// Type-safe error handling
def secured(handler: Handler): Handler = request =>
request.context.get("auth") match {
case Some(_) => handler(request)
case None => request.failAuth("Unauthorized")
}
Common Patterns
Middleware Composition
// Compose multiple middleware
val api = Router()
.use(auth) // Authentication
.use(rateLimit) // Rate limiting
.use(compress) // Compression
server.use("/api", api)
Error Handling Chain
// Chain of responsibility pattern
def errorHandler(error: ServerError): Future[Result] = error match {
case ValidationError(msg) =>
Map("error" -> msg).asJson(400)
case AuthError(msg) =>
Map("error" -> msg).asJson(401)
case _ =>
Map("error" -> "Internal error").asJson(500)
}
Request Context
// Share data between middleware
val withUser = request => {
val userId = request.params("id")
val user = getUserById(userId)
Future.successful(Continue(
request.copy(context = request.context + ("user" -> user))
))
}
val requireAdmin = request => {
request.context.get("user")
.collect { case user: User if user.isAdmin =>
Continue(request)
}
.getOrElse(Fail(AuthError("Admin required")))
}
Testing
Unit Testing
// Test handler directly
val request = Request.fromServerRequest(mockServerRequest(
method = "GET",
url = "/users/123",
params = Map("id" -> "123")
))
handler(request).map { result =>
result shouldBe Complete(
Response.json(user, 200))
}
Integration Testing
// Test full server
val port = 3000
var httpServer: NodeServer = null
// Set up server before tests
override def beforeAll(): Unit = {
val server = Server()
.use(authMiddleware)
.get("/users/:id", handler)
httpServer = server.listen(port) {}
}
// Clean up after tests
override def afterAll(): Unit = {
if (httpServer != null) {
httpServer.close(() => ())
}
}
// Test endpoints
"Server endpoints" - {
"should handle GET /users/:id" in {
fetch(s"http://localhost:$port/users/123")
.toFuture
.flatMap(response => {
response.status shouldBe 200
response.json().toFuture
})
.map(json => {
// Verify response JSON
json.id shouldBe "123"
})
}
}
Performance Considerations
- Request Processing
- Use streaming for large bodies
- Avoid unnecessary parsing
- Leverage type-safe parsing
- Middleware
- Order middleware by frequency
- Use conditional middleware
- Minimize context modifications
- Response Generation
- Use compression when appropriate
- Stream large responses
- Leverage caching headers
Node.js Integration
Using Node.js Modules
// Access Node.js functionality
import io.github.edadma.nodejs.fs
val stats = fs.promises.stat(path)
val stream = fs.createReadStream(path)
Express.js Migration
// Express.js
app.use(express.json())
app.post("/users", (req, res) => {
const user = req.body
res.json(user)
})
app.get("/users/:id", (req, res) => {
res.json({ id: req.params.id })
})
// Apion equivalent
server
.post("/users", request => {
// Built-in body parsing
request.json[User].flatMap {
case Some(user) => user.asJson
case None => "Invalid JSON".asText(400)
}
})
.get("/users/:id", request =>
Map("id" -> request.params("id")).asJson
)