HTTP Guide
This guide covers HTTP-specific features in Goa, from basic routing to advanced topics like WebSocket streaming and content negotiation.
HTTP Routing
Basic Routing
Routes are defined using the HTTP function within a Service:
var _ = Service("calculator", func() {
HTTP(func() {
Path("/calculator") // Base path for all endpoints
})
Method("add", func() {
Payload(func() {
Field(1, "a", Int, "First operand")
Field(2, "b", Int, "Second operand")
})
Result(Int)
HTTP(func() {
POST("/add") // POST /calculator/add
})
})
})
Goa supports all standard HTTP methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE.
A single method can handle multiple HTTP methods or paths:
Method("manage_user", func() {
Payload(User)
Result(User)
HTTP(func() {
POST("/users") // Create
PUT("/users/{user_id}") // Update
Response(StatusOK)
Response(StatusCreated)
})
})
Path Parameters
Capture dynamic values from the URL:
Method("get_user", func() {
Payload(func() {
Field(1, "user_id", String, "User ID")
})
Result(User)
HTTP(func() {
GET("/users/{user_id}") // Maps to payload.UserID
})
})
Map URL parameter names to payload field names:
Method("get_user", func() {
Payload(func() {
Field(1, "id", Int, "User ID")
})
HTTP(func() {
GET("/users/{user_id:id}") // URL uses user_id, maps to payload.ID
})
})
Query Parameters
Define query parameters with the Param function:
Method("list_users", func() {
Payload(func() {
Field(1, "page", Int, "Page number", func() {
Default(1)
Minimum(1)
})
Field(2, "per_page", Int, "Items per page", func() {
Default(20)
Minimum(1)
Maximum(100)
})
})
Result(CollectionOf(User))
HTTP(func() {
GET("/users")
Param("page")
Param("per_page")
})
})
Wildcards
Capture all remaining path segments:
Method("serve_files", func() {
Payload(func() {
Field(1, "path", String, "Path to file")
})
HTTP(func() {
GET("/files/*path") // Matches /files/docs/image.png
})
})
Service Relationships
Use Parent to establish service hierarchies:
var _ = Service("users", func() {
HTTP(func() {
Path("/users/{user_id}")
CanonicalMethod("get") // Override default "show"
})
Method("get", func() {
Payload(func() {
Field(1, "user_id", String)
})
HTTP(func() {
GET("") // GET /users/{user_id}
})
})
})
var _ = Service("posts", func() {
Parent("users") // Inherit parent's path
Method("list", func() {
// user_id inherited from parent
HTTP(func() {
GET("/posts") // GET /users/{user_id}/posts
})
})
})
Path Prefix Hierarchy
Combine prefixes at API and service levels:
var _ = API("myapi", func() {
HTTP(func() {
Path("/api") // Global prefix
})
})
var _ = Service("users", func() {
HTTP(func() {
Path("/v1/users") // Service prefix
})
Method("show", func() {
HTTP(func() {
GET("/{id}") // Final: /api/v1/users/{id}
})
})
})
URL-Encoded Form Requests
Use FormRequest() inside a method’s HTTP expression when an endpoint expects application/x-www-form-urlencoded, such as an OAuth token endpoint or a browser form. Goa generates the typed client encoder and server decoder; you do not need custom codec functions. Responses keep their existing encoding.
This method accepts an account ID in the path, a request ID in a header, and two form fields in the body:
Method("update_labels", func() {
Payload(func() {
Field(1, "accountId", String, "Account whose labels are updated")
Field(2, "requestId", String, "Identifier used to correlate the request")
Field(3, "label", String, "Display label for the account")
Field(4, "tags", ArrayOf(String), "Tags assigned to the account", func() {
MinLength(1)
})
Required("accountId", "requestId", "label", "tags")
})
HTTP(func() {
POST("/accounts/{account_id}/labels")
Param("accountId:account_id")
Header("requestId:X-Request-ID")
FormRequest()
Body(func() {
Attribute("label:display_name", String)
Attribute("tags:tag", ArrayOf(String))
Required("label", "tags")
})
Response(StatusNoContent)
})
})
Param("accountId:account_id") and Header("requestId:X-Request-ID") remove those attributes from the body. The Body mappings retain the payload names label and tags while selecting the form keys display_name and tag. A request body can therefore be:
display_name=Hello+Goa&tag=go&tag=api
For renamed fields in an explicit Body expression, declare the body types and required fields explicitly; implicit type inheritance uses exact attribute names.
- The final body must be a nonempty object of primitive fields or arrays of primitives. Named types using those shapes are supported. Nested objects, maps,
Any, and custom Go field types fail design evaluation. - Arrays use repeated keys, as shown by
tag=go&tag=api. Required arrays need at least one transmitted value; a form cannot express a present array with zero values. Bytes use standard base64. - Scalar keys may appear only once. An empty string is a present value, distinct from an omitted field. Authored defaults and validation still apply.
- The server reads body fields only from the body; query parameters cannot supply missing form values. Incorrect content types, malformed values, repeated scalar keys, and validation failures are rejected before service invocation. Unknown form fields are allowed.
FormRequest()is for ordinary HTTP endpoints. It cannot be combined with JSON-RPC,MultipartRequest(), orSkipRequestBodyEncodeDecode().
Regenerate after adding FormRequest() with a Goa version that includes this DSL. Switching an existing endpoint from JSON to forms changes its wire contract: update and deploy its clients and server together. Generated OpenAPI describes the form media type and mapped field names. Application middleware remains responsible for request size limits.
Content Negotiation
Built-in Encoders
Goa’s default encoders support:
- JSON (
application/json,*+json) - XML (
application/xml,*+xml) - Gob (
application/gob,*+gob) - HTML (
text/html) - Plain text (
text/plain)
Response content type is determined by:
AcceptheaderContent-Typeheader (if no Accept)- Default (JSON)
Set a default response content type:
Method("create", func() {
HTTP(func() {
POST("/media")
Response(StatusCreated, func() {
ContentType("application/json")
})
})
})
Custom Encoders
Create custom encoders for specialized formats:
type MessagePackEncoder struct {
w http.ResponseWriter
}
func (enc *MessagePackEncoder) Encode(v interface{}) error {
enc.w.Header().Set("Content-Type", "application/msgpack")
return msgpack.NewEncoder(enc.w).Encode(v)
}
func NewMessagePackEncoder(ctx context.Context, w http.ResponseWriter) goahttp.Encoder {
return &MessagePackEncoder{w: w}
}
Register custom encoders when creating the server:
func main() {
decoder := func(r *http.Request) goahttp.Decoder {
switch r.Header.Get("Content-Type") {
case "application/msgpack":
return NewMessagePackDecoder(r)
default:
return goahttp.RequestDecoder(r)
}
}
encoder := func(ctx context.Context, w http.ResponseWriter) goahttp.Encoder {
if accept := ctx.Value(goahttp.AcceptTypeKey).(string); accept == "application/msgpack" {
return NewMessagePackEncoder(ctx, w)
}
return goahttp.ResponseEncoder(ctx, w)
}
server := myapi.NewServer(endpoints, mux, decoder, encoder, nil, nil)
}
WebSocket Integration
Design Recap: Streaming is defined at the design level using
StreamingPayloadandStreamingResult. The DSL is transport-agnostic — the same design works for HTTP (WebSocket/SSE) and gRPC. See DSL Reference: Streaming for design patterns. This section covers HTTP-specific WebSocket implementation.
WebSocket enables real-time, bidirectional communication. Goa implements WebSocket through its streaming DSL.
Streaming Patterns
Client-to-Server Streaming:
Method("listener", func() {
StreamingPayload(func() {
Field(1, "message", String, "Message content")
Required("message")
})
HTTP(func() {
GET("/listen") // WebSocket endpoints must use GET
})
})
Server-to-Client Streaming:
Method("subscribe", func() {
StreamingResult(func() {
Field(1, "message", String, "Update content")
Field(2, "action", String, "Action type")
Field(3, "timestamp", String, "When it happened")
Required("message", "action", "timestamp")
})
HTTP(func() {
GET("/subscribe")
})
})
Bidirectional Streaming:
Method("echo", func() {
StreamingPayload(func() {
Field(1, "message", String, "Message to echo")
Required("message")
})
StreamingResult(func() {
Field(1, "message", String, "Echoed message")
Required("message")
})
HTTP(func() {
GET("/echo")
})
})
WebSocket Implementation
Server-side implementation:
func (s *service) handleStream(ctx context.Context, stream Stream) error {
connID := generateConnectionID()
s.registerConnection(connID, stream)
defer s.cleanupConnection(connID)
errChan := make(chan error, 1)
go func() {
errChan <- s.handleIncoming(stream)
}()
select {
case <-ctx.Done():
return ctx.Err()
case err := <-errChan:
return err
}
}
Connection management:
type ConnectionManager struct {
connections map[string]*ManagedConnection
mu sync.RWMutex
}
func (cm *ConnectionManager) AddConnection(id string, stream Stream) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.connections[id] = &ManagedConnection{
ID: id,
Stream: stream,
LastPing: time.Now(),
}
}
Server-Sent Events
Design Recap: SSE uses
StreamingResultat the design level withServerSentEvents()in the HTTP mapping. See DSL Reference: Streaming for design patterns.
SSE provides one-way server-to-client streaming over HTTP. It’s ideal for:
- Real-time notifications
- Live data feeds
- Progress updates
- Event streaming
SSE Design
var Event = Type("Event", func() {
Attribute("message", String, "Message body")
Attribute("timestamp", Int, "Unix timestamp")
Required("message", "timestamp")
})
var _ = Service("sse", func() {
Method("stream", func() {
StreamingResult(Event)
HTTP(func() {
GET("/events/stream")
ServerSentEvents() // Use SSE instead of WebSocket
})
})
})
Customize SSE events:
var Event = Type("Event", func() {
Attribute("message", String, "Message body")
Attribute("type", String, "Event type")
Attribute("id", String, "Event ID")
Attribute("retry", Int, "Reconnection delay in ms")
Required("message", "type", "id")
})
Method("stream", func() {
StreamingResult(Event)
HTTP(func() {
GET("/events/stream")
ServerSentEvents(func() {
SSEEventData("message")
SSEEventType("type")
SSEEventID("id")
SSEEventRetry("retry")
})
})
})
Handle Last-Event-Id for resumable streams:
Method("stream", func() {
Payload(func() {
Attribute("startID", String, "Last event ID received")
})
StreamingResult(Event)
HTTP(func() {
GET("/events/stream")
ServerSentEvents(func() {
SSERequestID("startID") // Maps Last-Event-Id header
})
})
})
SSE Implementation
func (s *Service) Stream(ctx context.Context, stream sse.StreamServerStream) error {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
event := &sse.Event{
Message: "Hello from server!",
Timestamp: time.Now().Unix(),
}
if err := stream.Send(event); err != nil {
return err
}
case <-ctx.Done():
return nil
}
}
}
Browser client:
const eventSource = new EventSource('/events/stream');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
};
eventSource.onerror = (error) => {
console.error('EventSource failed:', error);
eventSource.close();
};
CORS Configuration
The CORS plugin handles cross-origin requests. Import it:
import (
cors "goa.design/plugins/v3/cors/dsl"
. "goa.design/goa/v3/dsl"
)
API-level CORS:
var _ = API("calc", func() {
cors.Origin("http://127.0.0.1", func() {
cors.Headers("X-Shared-Secret")
cors.Methods("GET", "POST")
cors.Expose("X-Time")
cors.MaxAge(600)
cors.Credentials()
})
})
Service-level CORS:
var _ = Service("calc", func() {
// Allow specific origin
cors.Origin("localhost")
// Allow subdomain pattern
cors.Origin("*.domain.com", func() {
cors.Headers("X-Shared-Secret", "X-Api-Version")
cors.MaxAge(100)
cors.Credentials()
})
// Allow all origins
cors.Origin("*")
// Allow regex pattern
cors.Origin("/.*domain.*/", func() {
cors.Headers("*")
cors.Methods("GET", "POST")
cors.Expose("X-Time")
})
})
Static Content
Design Recap: Static file serving uses the
FilesDSL function. This is an HTTP-only feature. See DSL Reference: Static Files for design patterns.
Serve static files using the Files function:
var _ = Service("web", func() {
// Serve files from a directory
Files("/static/{*path}", "./public")
// Serve a specific file
Files("/favicon.ico", "./public/favicon.ico")
})
For Single Page Applications, serve the index.html for all routes:
var _ = Service("spa", func() {
// API endpoints
Method("api", func() {
HTTP(func() {
GET("/api/data")
})
})
// Serve SPA - catch-all for client-side routing
Files("/{*path}", "./dist/index.html")
})
See Also
- DSL Reference: Streaming — Design-level streaming patterns
- DSL Reference: Static Files — Files DSL for static content
- DSL Reference: Error Handling — Design-level error definitions
- gRPC Guide — gRPC transport features
- Error Handling Guide — Complete error handling patterns
- Clue Documentation — HTTP middleware for observability
Best Practices
URL Design
- Use nouns for resources:
/articles, not/list-articles - Use plural nouns consistently
- Let HTTP methods define actions
- Keep URLs hierarchical and predictable
Error Handling
- Map errors to appropriate HTTP status codes
- Use consistent error response formats
- Include meaningful error messages
Performance
- Use appropriate buffer sizes for WebSocket
- Implement connection pooling for high-traffic services
- Consider message batching for streaming endpoints
Security
- Always use HTTPS in production
- Configure CORS appropriately
- Validate all input parameters
- Set appropriate timeouts for long-lived connections