Skip to content

Routing

Ada provides a flexible HTTP routing system that supports static routes, parameterized routes, and wildcard matching.

The routing system doesn't use http Method, so you can use any method you want.

Route Patterns

Routing system follows a specific priority order when matching routes:

  1. Static routes - Exact matches have highest priority
  2. Parameterized routes - Routes with path parameters
  3. Wildcard routes - Routes with * or {name...} captures
go
server.GET("/users/new", newUserForm)      // Highest priority
server.GET("/users/{id}", getUser)         // Medium priority
server.GET("/users/*", catchAllUsers)      // Lowest priority

A request to /users/new will match the static route, not the parameterized route.

Static Routes

Static routes match exact path segments:

go
server.GET("/", homeHandler)
server.GET("/about", aboutHandler)
server.GET("/api/v1/status", statusHandler)

Parameterized Routes

Parameterized routes capture a single path segment and make it available via r.PathValue():

go
server.GET("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
    userID := r.PathValue("id")
    fmt.Fprintf(w, "User ID: %s", userID)
})

server.GET("/posts/{postID}/comments/{commentID}", func(w http.ResponseWriter, r *http.Request) {
    postID := r.PathValue("postID")
    commentID := r.PathValue("commentID")
    fmt.Fprintf(w, "Post: %s, Comment: %s", postID, commentID)
})

{name} matches exactly one segment. It does not cross / boundaries — use the greedy {name...} form below when you need that.

Wildcard Routes

Ada has two wildcard forms:

FormPositionMatchesAccess via
*Middle or trailing (1 per route)One segment if middle, rest of the path if trailingr.PathValue("*")
{name...}Trailing onlyRest of the path (incl. /)r.PathValue("name")

{name...} is just a named alias for a trailing * — same matching, only the PathValue key differs. Use it when a descriptive name reads better than "*", especially in routes that already have another capture.

go
// Trailing `*` — anonymous greedy capture.
server.GET("/files/*", func(w http.ResponseWriter, r *http.Request) {
    rest := r.PathValue("*")
    // GET /files/a/b/c.txt → rest == "a/b/c.txt"
})

// Middle `*` — exactly one segment, does not cross `/`.
server.POST("/api/v1/external/*/test", func(w http.ResponseWriter, r *http.Request) {
    name := r.PathValue("*")
    // POST /api/v1/external/myname/test → name == "myname"
    // POST /api/v1/external/a/b/test    → 404
})

// Trailing `{name...}` — named greedy capture.
server.GET("/files/{path...}", func(w http.ResponseWriter, r *http.Request) {
    path := r.PathValue("path")
    // GET /files/a/b/c.txt → path == "a/b/c.txt"
    // GET /files/          → path == ""
    // GET /files           → 404 (separator required)
})

Combining captures

Stack any number of {name} params; the wildcard rules only apply to * and {name...}. Each capture lives under its own key.

go
// Middle `{name}` + named greedy trailing.
server.GET("/users/{name}/files/{path...}", func(w http.ResponseWriter, r *http.Request) {
    name := r.PathValue("name") // e.g. "alice"
    path := r.PathValue("path") // e.g. "docs/note.md"
})

// Multiple single-segment params + greedy trailing.
server.GET("/orgs/{org}/users/{user}/files/{path...}", h)

Validation rules

Ada panics at registration time on ambiguous patterns — bad routes fail loud at boot:

  1. At most one * per route. Both would map to PathValue("*"). Use {name} for the other capture.
  2. At most one {name...}, and it must be trailing. A greedy match consumes the rest of the path by definition, so it can't sit in the middle (the matcher would have nothing to stop against) and can't appear twice (the first one already ate everything).
go
server.GET("/a/*/b/*", h)        // panics: more than one '*'
server.GET("/a/{x...}/b", h)     // panics: greedy must be trailing

Route Groups

Route groups allow you to organize routes with common prefixes and middleware:

go
server := ada.New()

// Create an API v1 group
apiV1 := server.Group("/api/v1")
apiV1.GET("/users", getUsers)
apiV1.POST("/users", createUser)
apiV1.DELETE("/users/{id}", deleteUser)

// Create an admin group with audit middleware
admin := server.Group("/admin", auditMiddleware)
admin.GET("/dashboard", adminDashboard)
admin.GET("/users", adminUsers)
admin.POST("/users/{id}/ban", banUser)

Groups can be nested for more complex organization:

go
api := server.Group("/api")
v1 := api.Group("/v1")
v1.GET("/users", getUsersV1)

v2 := api.Group("/v2")
v2.GET("/users", getUsersV2)

HTTP Methods

Ada supports all standard HTTP methods through dedicated methods:

go
server := ada.New()

server.GET("/users", getUsers)
server.POST("/users", createUser)
server.PUT("/users/{id}", updateUser)
server.PATCH("/users/{id}", patchUser)
server.DELETE("/users/{id}", deleteUser)
server.HEAD("/users/{id}", headUser)
server.OPTIONS("/users", optionsUsers)
server.TRACE("/trace", traceHandler)
server.CONNECT("/connect", connectHandler)
server.QUERY("/search", searchHandler)

The QUERY method is a safe and idempotent HTTP method that carries the query in the request body (RFC 10008 - The HTTP QUERY Method). Since net/http does not define a constant for it yet, ada exposes ada.MethodQuery.

Other not standard HTTP methods can be added with HandleWithMethod

go
server.HandleWithMethod("FOO", "/foo", fooHandler)

Method-Agnostic Routing

You can also register handlers that respond to any HTTP method:

go
server.HandleFunc("/health", healthCheck)
server.Handle("/static", http.FileServer(http.Dir("./static")))

Automatic HEAD and OPTIONS

Ada automatically handles HEAD and OPTIONS requests:

  • HEAD: If a GET handler is registered, HEAD requests are served by the same handler. Go's http.ResponseWriter automatically suppresses the response body. Explicit HEAD handlers take priority.
  • OPTIONS: Returns 204 No Content with an Allow header listing available methods (e.g. Allow: GET, HEAD, OPTIONS, POST). Explicit OPTIONS handlers take priority.

405 Method Not Allowed

When a request matches a path but not any registered method, Ada returns 405 Method Not Allowed with an Allow header listing the available methods:

GET /users         → 200 OK
POST /users        → 405 Method Not Allowed (Allow: GET, HEAD, OPTIONS)
GET /nonexistent   → 404 Not Found

Custom 404 Handler

You can set a custom handler for routes that don't match:

go
server := ada.New()
server.NotFound(func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusNotFound)
    fmt.Fprintf(w, "Page not found: %s", r.URL.Path)
})

Custom 405 Handler

You can set a custom handler for method-not-allowed responses. The Allow header is always set before the handler is called:

go
server := ada.New()
server.MethodNotAllowed(func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusMethodNotAllowed)
    w.Write([]byte(`{"error":"method not allowed","allow":"` + w.Header().Get("Allow") + `"}`))
})