A Caddy handle is a directive that processes a request, while a route is a collection of handle directives that are matched against a request’s path and method.
Let’s see Caddy in action. Imagine you have a simple website and you want to serve static files, but also have a specific endpoint for an API.
example.com {
root * /var/www/html
file_server
route /api/* {
reverse_proxy localhost:8080
}
}
In this configuration:
example.comis the site address.root * /var/www/htmlsets the document root for all requests.file_serverenables serving static files from the root.route /api/*defines a route that matches any request starting with/api/.reverse_proxy localhost:8080is ahandlethat forwards requests matching the route to a backend service running onlocalhost:8080.
The route directive is essentially a conditional wrapper. It says, "If the request matches this path and method (method is optional), then execute the directives within this block." The directives inside the route block are handles.
A handle directive is a specific action Caddy can take. Common handles include:
file_server: Serves static files.reverse_proxy: Forwards requests to another server.rewrite: Changes the request’s URL.redirect: Sends a redirect response to the client.log: Logs request details.encode: Handles compression (gzip, brotli).
You can also define custom handles using the handle directive itself, often in conjunction with handle_errors or handle_response.
When a request comes in, Caddy iterates through all defined routes. The first route whose path and method match the incoming request will have its internal handles executed in order. If no route matches, Caddy looks for top-level handles (those not inside a route).
This distinction is crucial for organizing your Caddyfile. You use route to define when a set of actions should occur, and handle to define what those actions are.
Consider a more complex scenario: serving a SPA with a fallback API.
example.com {
root * /var/www/app
file_server
# Handle API requests
route /api/* {
rewrite /api/* /api/{1} # Example rewrite for API
reverse_proxy localhost:8080
}
# Handle SPA fallback for non-API routes
route {
# If the request is not for an API and doesn't match a file,
# serve index.html
try_files {path} {path}/ /index.html
file_server
}
}
Here, the first route handles API requests. If a request doesn’t match /api/*, Caddy moves to the second route. This second, un-pathed route acts as a catch-all. try_files is a handle that attempts to find a file matching the request path, and if it doesn’t, it falls back to /index.html. This is standard for SPAs where the client-side router handles routing after the initial load.
The power of route comes from its specificity and ordering. If you had a route /admin/* before your /api/* route, any /admin/api/something request would be handled by the admin route first.
One subtle but powerful aspect is that route directives are processed in the order they appear in the Caddyfile, and the first matching route "wins" for that request. This means you can have general routes and then more specific ones that take precedence if placed earlier. However, if you don’t specify a path for a route, it acts as a catch-all for any request that hasn’t been matched by a previous, more specific route.
When you define multiple handle directives within a single route, they are executed sequentially. This allows you to chain operations, like rewriting a URL and then proxying it.
The key takeaway is that route provides the conditional logic (the "when"), and handle provides the action (the "what"). You use route to segment your Caddyfile based on request characteristics like path and method, and handles within those routes to perform the actual request processing.
The next logical step is understanding how handle_errors and handle_response allow you to hook into Caddy’s processing pipeline at different stages.