Getting Started

March 30, 2026 ยท View on GitHub

Installation

Add gala-server as a dependency in your MODULE.bazel:

bazel_dep(name = "gala-server", version = "0.1.0")

Quick Start

package main

import . "martianoff/gala-server"

func main() {
    val server = NewServer().
        GET("/", (req) => Ok("Hello, GALA!")).
        GET("/health", (req) => JsonResponse("{\"status\": \"ok\"}")).
        WithFilter(Logger()).
        WithFilter(Recovery())

    server.ListenGraceful()
}

Core Concepts

Server

NewServer() creates an immutable HTTP server builder. Chain methods to add routes and filters, then call Listen() or ListenGraceful() to start.

val server = NewServer().
    GET("/users", listUsers).
    POST("/users", createUser).
    WithFilter(Logger()).
    ListenGraceful()

Handlers

Handlers are functions that take a Request and return a Response:

func hello(req Request) Response = Ok("Hello!")

func getUser(req Request) Response {
    val id = req.Param("id").GetOrElse("0")
    return JsonResponse(s"{\"id\": \"$id\"}")
}

Request

Access request data through the Request type:

req.Method()                    // sealed type: GET(), POST(), CUSTOM("PURGE"), etc.
req.Path()                      // "/users/123"
req.Body()                      // request body as string
req.Param("id")                 // path param -> Option[string]
req.QueryParam("page")          // query param -> Option[string]
req.Header("Authorization")     // header -> Option[string]
req.ContentType()               // Content-Type header -> Option[string]

Response Builders

Convenience functions for common HTTP responses:

// Status codes
Ok("body")                      // 200
Created("body")                 // 201
Accepted("body")                // 202
NoContent()                     // 204
BadRequest("body")              // 400
Unauthorized("body")            // 401
Forbidden("body")               // 403
NotFound("body")                // 404
UnprocessableEntity("body")     // 422
TooManyRequests("body")         // 429
InternalError("body")           // 500
BadGateway("body")              // 502
ServiceUnavailable("body")      // 503

// Content types
JsonResponse("{\"key\": \"value\"}")   // 200 + application/json
Text("plain text")                     // 200 + text/plain
Html("<h1>Hello</h1>")                 // 200 + text/html
JsonWithStatus(StatusCreated(), "{\"id\": 1}")

// Redirects
Redirect("/new-url")            // 302
RedirectPermanent("/new-url")   // 301

// Chaining
Ok("hello").
    WithHeader("X-Custom", "value").
    WithStatus(StatusAccepted())

Route Patterns

Patterns use Go 1.22+ syntax:

server.GET("/users", handler)           // exact match
server.GET("/users/{id}", handler)      // path parameter
server.GET("/files/{path...}", handler) // wildcard (rest of path)

TLS / HTTPS

Serve over HTTPS by providing a certificate and key:

val server = NewServer().
    WithPort(443).
    GET("/", (req) => Ok("Secure!")).
    WithFilter(Logger())

// Basic TLS
server.ListenTLS("cert.pem", "key.pem")

// TLS with graceful shutdown
server.ListenGracefulTLS("cert.pem", "key.pem")

Type-Safe Extractors

Instead of manually unwrapping Option values from request parameters, use extractors that return Try[T] for clean error handling:

import . "martianoff/gala-server"

func getUser(req Request) Response =
    PathParamInt(req, "id") match {
        case Success(id) => Ok(s"User $id")
        case Failure(err) => BadRequest(s"Invalid ID: $err")
    }

func search(req Request) Response {
    val query = QueryRequired(req, "q")
    val page = QueryInt(req, "page", 1)
    return query match {
        case Success(q) => Ok(s"Searching for $q, page $page")
        case Failure(err) => BadRequest(err.Error())
    }
}

Available extractors: PathParam, PathParamInt, QueryRequired, QueryInt, HeaderRequired, BodyAs[T].

Content Negotiation

Respond with different formats based on the client's Accept header:

func handler(req Request) Response =
    Negotiate(req,
        () => JsonResponse("{\"msg\": \"hello\"}"),
        () => Html("<p>hello</p>"),
        () => Text("hello"),
    )

Metrics (Prometheus)

Track request count, latency, and status codes with a built-in Prometheus endpoint:

val stats = NewMetrics()

val server = NewServer().
    WithFilter(MetricsFilter(stats)).
    WithMetricsEndpoint("/metrics", stats).
    GET("/hello", (req) => Ok("hello")).
    ListenGraceful()

Visit /metrics to see Prometheus-format stats: gala_requests_total, gala_responses_total{status}, gala_route_latency_avg_ms{route}, and more.

Sessions

Cookie-based in-memory sessions for stateful request handling:

val sessions = NewSessions("my-secret")

val server = NewServer().
    WithFilter(SessionFilter(sessions)).
    GET("/login", (req) => {
        req.SessionSet("user", "alice")
        return Ok("logged in")
    }).
    GET("/profile", (req) => {
        val user = req.SessionGet("user").GetOrElse("anonymous")
        return Ok(s"Hello, $user")
    })

Sessions are stored in-memory with concurrent access support. Configure with WithCookieName, WithMaxAge, WithSecure.

Next Steps

  • API Reference -- complete reference for all types, constructors, metrics, sessions, and filters
  • Filters -- filter system with three scopes, filter algebra, and resilience patterns
  • Route Groups -- organizing routes with shared prefixes and filters