Serve and route requests
Match and handle requests
Use named matchers and mutually exclusive handle blocks to make routing decisions visible.
10 minute lesson
Real sites don’t give the same answer to every request. You need routing, and in the Caddyfile routing is built from matchers and handle blocks.
A matcher selects requests by path, method, host, header, query, and other facts about the request. A named matcher starts with @: you define it once, then reference it from directives.
:8080 {
@health path /health
handle @health {
respond "ok" 200
}
handle {
respond "application"
}
}
@health matches requests whose path is exactly /health. The first handle block runs only for those. The second handle has no matcher, so it acts as the fallback for everything else.
Test both routes:
curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/anything-else
The key property: sibling handle blocks are mutually exclusive. Exactly one of them runs per request, like the branches of a switch statement. Caddy orders them so blocks with more specific path matchers are tried first, and a matcher-less handle catches whatever remains. That makes routing decisions something you can read straight off the file.
To see what Caddy built from this, adapt the configuration:
caddy adapt --config Caddyfile --pretty
Find the two routes in the JSON. Each handle became a subroute, and the health route carries the path matcher you wrote. When a request takes a branch you didn’t expect, this output settles the argument.
Always keep a final fallback handle when every request needs an answer. Without it, an unmatched request falls through to whatever comes after your routing — often an empty 200 response that looks like success in your monitoring while users see a blank page. That bug is far easier to prevent than to notice.
Lesson completed