Goa provides a powerful routing system that maps HTTP requests to your service methods. This guide covers:
In Goa, routes are defined in your design using the HTTP
function within a Service definition. The HTTP
function allows you to specify how your service methods are exposed over HTTP.
Here’s a basic example:
var _ = Service("calculator", func() {
// Define service-wide HTTP settings
HTTP(func() {
// Set a base path for all endpoints in this service
Path("/calculator")
})
Method("add", func() {
// Define method payload
Payload(func() {
// Field order matters - tag 1 is first
Field(1, "a", Int, "First operand")
Field(2, "b", Int, "Second operand")
})
// Define method result
Result(Int)
// Define HTTP transport
HTTP(func() {
POST("/add") // Handles POST /calculator/add
})
})
})
The above example:
Goa supports all standard HTTP methods through dedicated DSL functions: GET
, POST
, PUT
, DELETE
, PATCH
, HEAD
, OPTIONS
, and TRACE
. A single service method can handle multiple HTTP methods or paths:
Method("manage_user", func() {
Description("Create or update a user")
Payload(User)
Result(User)
HTTP(func() {
POST("/users") // Create user
PUT("/users/{user_id}") // Update existing user
Response(StatusOK) // 200 for updates
Response(StatusCreated) // 201 for creation
})
})
You can capture dynamic values from the URL path using parameters. Path parameters are defined using curly braces {parameter_name}
and are automatically mapped to payload fields.
Method("get_user", func() {
Description("Retrieve a user by their ID")
Payload(func() {
// The user_id field will be populated from the URL path
Field(1, "user_id", String, "User ID from the URL path")
})
Result(User)
HTTP(func() {
GET("/users/{user_id}") // Maps {user_id} to payload.UserID
})
})
In Goa, parameter types are defined in the payload definition, not in the URL pattern. The URL pattern only defines how to map transport names to payload fields.
Simple Payload with Primitive Type
Method("get_user", func() {
// When payload is a primitive type, it maps directly to the path parameter
Payload(String, "User ID")
Result(User)
HTTP(func() {
GET("/users/{user_id}") // user_id value becomes the payload
})
})
Structured Payload with Direct Mapping
Method("get_user", func() {
Payload(func() {
// Parameter type (Int) is defined here in the payload
Field(1, "user_id", Int, "User ID")
})
Result(User)
HTTP(func() {
GET("/users/{user_id}") // Maps directly to payload.UserID
})
})
Transport Name Mapping
Method("get_user", func() {
Payload(func() {
// Internal field name is "id"
Field(1, "id", Int, "User ID")
})
HTTP(func() {
// Use user_id in URL but map to payload.ID
GET("/users/{user_id:id}")
})
})
The {name:field}
syntax in the path pattern is used for name mapping only:
name
is what appears in the URLfield
is the name of the field in your payloadFor primitive payloads, the path parameter value becomes the entire payload:
Method("download", func() {
// Entire payload is a string representing the file path
Payload(String, "Path to file")
HTTP(func() {
GET("/files/{*path}") // Captured path becomes the payload
})
}
Method("get_version", func() {
// Payload is a simple integer
Payload(Int, "API version number")
HTTP(func() {
GET("/api/{version}") // version number becomes the payload
})
})
When using structured payloads, you can combine path parameters with other payload fields:
Method("update_user_profile", func() {
Payload(func() {
// Path parameter
Field(1, "id", Int, "User ID")
// Body fields
Field(2, "name", String, "User's name")
Field(3, "email", String, "User's email")
})
HTTP(func() {
PUT("/users/{user_id:id}") // Maps URL's user_id to payload.ID
Body("name", "email") // These fields come from request body
})
})
### Query Parameters
Query string parameters are defined using the `Param` function and must correspond to payload fields. You can set default values and validation rules:
```go
Method("list_users", func() {
Description("List users with pagination")
Payload(func() {
Field(1, "page", Int, "Page number", func() {
Default(1) // Default to page 1
Minimum(1) // Page must be positive
})
Field(2, "per_page", Int, "Items per page", func() {
Default(20) // Default to 20 items
Minimum(1)
Maximum(100) // Limit maximum items
})
})
Result(CollectionOf(User))
HTTP(func() {
GET("/users")
// Map payload fields to query parameters
Param("page")
Param("per_page")
})
})
### Wildcards and Catch-all Routes
For flexible path matching, use the asterisk syntax (`*path`) to capture all remaining path segments. The captured value is available in the payload:
```go
Method("serve_files", func() {
Description("Serve static files from a directory")
Payload(func() {
// The path field will contain all segments after /files/
Field(1, "path", String, "Path to the file")
})
HTTP(func() {
GET("/files/*path") // Matches /files/docs/image.png
})
})
Use nouns to represent resources and let HTTP methods define the actions:
HTTP(func() {
// Good - HTTP method indicates the action
GET("/articles") // List articles
POST("/articles") // Create article
GET("/articles/{id}") // Get one article
PUT("/articles/{id}") // Update article
DELETE("/articles/{id}") // Delete article
// Avoid - action in URL
GET("/list-articles")
POST("/create-article")
})
Use plural nouns for collection endpoints and maintain consistency:
HTTP(func() {
// Good - consistent use of plural
GET("/users") // List users
GET("/users/{id}") // Get one user
POST("/users") // Create user
// Avoid mixing singular and plural
GET("/user") // Don't use singular
GET("/users/{id}") // Don't mix conventions
})
Goa allows you to define path prefixes at different levels of your API design:
var _ = API("myapi", func() {
HTTP(func() {
Path("/api") // Global prefix for all services
})
})
var _ = Service("users", func() {
HTTP(func() {
Path("/v1/users") // Prefix for all methods in this service
})
})
The final URL path is constructed by combining these prefixes in order. For example:
var _ = API("myapi", func() {
HTTP(func() {
Path("/api") // API-level prefix
})
Service("users", func() {
HTTP(func() {
Path("/v1/users") // Service-level prefix
})
Method("show", func() {
Payload(func() {
Field(1, "id", Int)
})
HTTP(func() {
GET("/{id}") // Method path
})
})
})
})
This results in the path /api/v1/users/{id}
for the show method.
Version your API using path prefixes:
var _ = Service("users", func() {
HTTP(func() {
Path("/v1") // All endpoints will be under /v1
})
Method("list", func() {
HTTP(func() {
GET("/users") // Final path: /v1/users
})
})
})
Goa provides the Parent
DSL to establish relationships between services. When you specify a parent service:
By default, Goa uses the “show” method as the canonical method for a service.
The canonical method’s HTTP path is used as the prefix for all child service
endpoints. You can override this using the CanonicalMethod
function in the
service’s HTTP expression.
Here’s an example:
var _ = Service("users", func() {
HTTP(func() {
Path("/users/{user_id}")
// Override the default "show" method
CanonicalMethod("get")
})
Method("get", func() {
Payload(func() {
Field(1, "user_id", String)
})
HTTP(func() {
GET("") // Results in /users/{user_id}
})
})
})
var _ = Service("posts", func() {
// Specify users as the parent service
Parent("users")
Method("list", func() {
// user_id is automatically inherited from parent's canonical method payload
HTTP(func() {
GET("/posts") // Results in /users/{user_id}/posts
})
})
})
In this example:
users
service specifies “get” as its canonical method instead of the default “show”/users/{user_id}
) becomes the prefix for all child service endpointsposts
service inherits this prefix and the user_id
parameter from the parent’s canonical methodlist
method becomes /users/{user_id}/posts
Express resource relationships through nested paths. This can be done either using the Parent DSL (as shown above) or by explicitly defining nested paths:
var _ = Service("social", func() {
// Define methods for user resources
Method("list_users", func() {
HTTP(func() {
GET("/users") // List all users
})
})
Method("get_user", func() {
Payload(func() {
Field(1, "user_id", String, "User ID")
})
HTTP(func() {
GET("/users/{user_id}") // Get a specific user
})
})
// Methods for posts under a user
Method("list_user_posts", func() {
Payload(func() {
Field(1, "user_id", String, "User ID")
Field(2, "limit", Int, "Maximum number of posts to return")
})
HTTP(func() {
GET("/users/{user_id}/posts") // List posts for a user
Param("limit") // Query parameter
})
})
Method("create_user_post", func() {
Payload(func() {
Field(1, "user_id", String, "User ID")
Field(2, "title", String, "Post title")
Field(3, "content", String, "Post content")
})
HTTP(func() {
POST("/users/{user_id}/posts") // Create a post for a user
Body("title", "content") // These fields go in the request body
})
})
// Methods for comments under a post
Method("list_post_comments", func() {
Payload(func() {
Field(1, "user_id", String, "User ID")
Field(2, "post_id", String, "Post ID")
})
HTTP(func() {
GET("/users/{user_id}/posts/{post_id}/comments") // List comments on a post
})
})
})
This approach:
You can also use service-wide path prefixes to group related endpoints:
var _ = Service("social", func() {
HTTP(func() {
Path("/v1/social") // Prefix for all endpoints in this service
})
Method("list_users", func() {
HTTP(func() {
GET("/users") // Final path: /v1/social/users
})
})
Method("get_user_posts", func() {
HTTP(func() {
GET("/users/{user_id}/posts") // Final path: /v1/social/users/{user_id}/posts
})
})
})
Goa generates all the necessary routing code based on your design. The generated code includes:
This means you can focus on implementing your business logic while Goa handles all the HTTP transport details.