Routing & Response
Route files registered, JSON responses shaped
Routing is the core of any web framework and Goravel keeps it clean. Before we get there, one tool that saves a lot of time.
Hot reload with Air
Every change means restarting the server, and that gets old fast. Air watches the project and restarts it for you.
go install github.com/air-verse/air@latest
air -v
From now on, instead of go run ., run:
air
Air watches .go, .tpl, .tmpl and .html files. Change main.go, save, and the server restarts on its own. You never touch the terminal.
The behaviour is in .air.toml at the project root. The defaults are fine; the one knob you might want is the restart delay:
[build]
cmd = "go build -o ./tmp/main.exe ."
delay = 1000
exclude_dir = ["storage", "database", "tmp"]
include_ext = ["go", "tpl", "tmpl", "html"]
Drop delay to 500 if you want it snappier. Start Air once and forget about it.
Registering a route
Open routes/web.go. Routes are registered through the Route facade, with a method per HTTP verb. The skeleton already has one:
facades.Route().Get("/", func(ctx http.Context) http.Response {
return ctx.Response().View().Make("welcome.tmpl", map[string]any{
"version": support.Version,
})
})
The first argument is the path, the second is the handler. A handler always has the same shape: it takes a Goravel http.Context and returns an http.Response. That is the contract for every handler in this course.
This one returns a view, an HTML template from resources/views. You are not limited to HTML.
Response types
A plain string:
facades.Route().Get("hello", func(ctx http.Context) http.Response {
return ctx.Response().String(200, "Hello, world")
})
Open it and check the network tab: the content type is text/plain. Headers are one call away, so the same body becomes HTML like this:
facades.Route().Get("hello", func(ctx http.Context) http.Response {
return ctx.Response().Header("Content-Type", "text/html").String(200, "<h1>Hello, world</h1>")
})
For an API, most of the time you want JSON. Json takes a status code and any value; a map is the simplest:
facades.Route().Get("hello", func(ctx http.Context) http.Response {
return ctx.Response().Json(200, map[string]any{
"message": "Hello, world",
})
})
There are others: File to send a file, Data for raw bytes with a content type, Stream to push data to the client as it is produced. We will meet them when we need them.
Grouping routes under a prefix
Every API route in this course lives under /api/v1. Versioning from day one is a production habit. Instead of writing the prefix on each route, put it on a group:
import "github.com/goravel/framework/contracts/route"
facades.Route().Prefix("api/v1").Group(func(router route.Router) {
router.Post("tasks", func(ctx http.Context) http.Response {
return ctx.Response().String(200, "task created")
})
})
Everything registered on router gets the prefix. POST is what we use when the client sends data to the server.
Testing with the HTTP client
You cannot make a POST from the address bar. Postman and curl both work; in this series I use the HTTP client built into GoLand, because the requests live in the repository next to the code. Create requests.http in the project root:
### Create task
POST http://localhost:3000/api/v1/tasks
### Hello
GET http://localhost:3000/hello
Each request starts with ### and a name, then the method and the URL. Note the prefix on the tasks route. Click run and the response appears in the editor. Push the file with the code and everyone on the project has the same requests, no collection to share around.
Named routes
A route can carry a name:
router.Post("tasks", func(ctx http.Context) http.Response {
return ctx.Response().String(200, "task created")
}).Name("tasks.create")
And you can ask the router about a route by name:
facades.Route().Get("hello", func(ctx http.Context) http.Response {
return ctx.Response().Json(200, facades.Route().Info("tasks.create"))
})
Hit /hello and you get the route’s path, name, method and handler. The point of naming is that code refers to tasks.create, not to a hardcoded path. Change the path later and everything that used the name still works.
Separate the API from web routes
routes/web.go is for web things: server-rendered pages, public files. API routes deserve their own file. Create routes/api.go:
package routes
import (
"taskflow/app/facades"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/route"
)
func Api() {
facades.Route().Prefix("api/v1").Group(func(router route.Router) {
router.Post("tasks", func(ctx http.Context) http.Response {
return ctx.Response().String(200, "task created")
}).Name("tasks.create")
})
}
Move the API routes out of web.go, save, and here is the gotcha: the routes are gone. No error, no warning, just 404s. Goravel does not discover route files. You register them in bootstrap/app.go:
WithRouting(func() {
routes.Api()
routes.Web()
}).
Save again and the routes are back. Every new route file needs this one line, and forgetting it fails silently, so make it a habit.
Listing routes
When you are not sure a route is registered, ask:
go run . artisan route:list
It prints every registered route with its method, path and name. Use it any time a request comes back 404 and you do not know why.
Next episode: reading data from incoming requests, the body, query parameters and route parameters.