After designing your REST API with Goa’s DSL, it’s time to implement the service. This tutorial walks you through the implementation process step by step.
goa gen
)main.go
to implement the service and the HTTP serverFrom your project root (e.g., concerts/
), run the Goa code generator:
goa gen concerts/design
This command analyzes your design file (design/design.go
) and produces a gen/
folder containing:
gen/concerts/
)gen/http/concerts/
) for both server and clientgen/http/
)Note: If you change your design (e.g., add methods or fields), rerun goa gen
to keep the generated code in sync.
Let’s explore the key components of the generated code. Understanding these files is crucial for implementing your service correctly and taking full advantage of Goa’s features.
Defines core service components independent of transport protocol:
service.go
)Contains server-side HTTP-specific logic:
Provides client-side HTTP functionality:
The gen/http
directory contains auto-generated OpenAPI specifications:
openapi3.yaml
and openapi3.json
(OpenAPI 3.0)These specifications are compatible with Swagger UI and other API tools, making them useful for API exploration and client generation.
The generated service interface in gen/concerts/service.go
defines the methods you need to implement:
type Service interface {
// List concerts with optional pagination. Returns an array of concerts sorted by date.
List(context.Context, *ListPayload) (res []*Concert, err error)
// Create a new concert entry. All fields are required to ensure complete concert information.
Create(context.Context, *ConcertPayload) (res *Concert, err error)
// Get a single concert by its unique ID.
Show(context.Context, *ShowPayload) (res *Concert, err error)
// Update an existing concert by ID. Only provided fields will be updated.
Update(context.Context, *UpdatePayload) (res *Concert, err error)
// Remove a concert from the system by ID. This operation cannot be undone.
Delete(context.Context, *DeletePayload) (err error)
}
Your implementation needs to:
Create a file at cmd/concerts/main.go
with the following implementation:
package main
import (
"context"
"fmt"
"log"
"net/http"
"github.com/google/uuid"
goahttp "goa.design/goa/v3/http"
// Use gen prefix for generated packages
genconcerts "concerts/gen/concerts"
genhttp "concerts/gen/http/concerts/server"
)
// ConcertsService implements the genconcerts.Service interface
type ConcertsService struct {
concerts []*genconcerts.Concert // In-memory storage
}
// List upcoming concerts with optional pagination.
func (m *ConcertsService) List(ctx context.Context, p *genconcerts.ListPayload) ([]*genconcerts.Concert, error) {
start := (p.Page - 1) * p.Limit
end := start + p.Limit
if end > len(m.concerts) {
end = len(m.concerts)
}
return m.concerts[start:end], nil
}
// Create a new concerts entry.
func (m *ConcertsService) Create(ctx context.Context, p *genconcerts.ConcertPayload) (*genconcerts.Concert, error) {
newConcert := &genconcerts.Concert{
ID: uuid.New().String(),
Artist: p.Artist,
Date: p.Date,
Venue: p.Venue,
Price: p.Price,
}
m.concerts = append(m.concerts, newConcert)
return newConcert, nil
}
// Get a single concert by ID.
func (m *ConcertsService) Show(ctx context.Context, p *genconcerts.ShowPayload) (*genconcerts.Concert, error) {
for _, concert := range m.concerts {
if concert.ID == p.ConcertID {
return concert, nil
}
}
// Use designed error
return nil, genconcerts.MakeNotFound(fmt.Errorf("concert not found: %s", p.ConcertID))
}
// Update an existing concert by ID.
func (m *ConcertsService) Update(ctx context.Context, p *genconcerts.UpdatePayload) (*genconcerts.Concert, error) {
for i, concert := range m.concerts {
if concert.ID == p.ConcertID {
if p.Artist != nil {
concert.Artist = *p.Artist
}
if p.Date != nil {
concert.Date = *p.Date
}
if p.Venue != nil {
concert.Venue = *p.Venue
}
if p.Price != nil {
concert.Price = *p.Price
}
m.concerts[i] = concert
return concert, nil
}
}
return nil, genconcerts.MakeNotFound(fmt.Errorf("concert not found: %s", p.ConcertID))
}
// Remove a concert from the system by ID.
func (m *ConcertsService) Delete(ctx context.Context, p *genconcerts.DeletePayload) error {
for i, concert := range m.concerts {
if concert.ID == p.ConcertID {
m.concerts = append(m.concerts[:i], m.concerts[i+1:]...)
return nil
}
}
return genconcerts.MakeNotFound(fmt.Errorf("concert not found: %s", p.ConcertID))
}
// main instantiates the service and starts the HTTP server.
func main() {
// Instantiate the service
svc := &ConcertsService{}
// Wrap it in the generated endpoints
endpoints := genconcerts.NewEndpoints(svc)
// Build an HTTP handler
mux := goahttp.NewMuxer()
requestDecoder := goahttp.RequestDecoder
responseEncoder := goahttp.ResponseEncoder
handler := genhttp.New(endpoints, mux, requestDecoder, responseEncoder, nil, nil)
// Mount the handler on the mux
genhttp.Mount(mux, handler)
// Create a new HTTP server
port := "8080"
server := &http.Server{Addr: ":" + port, Handler: mux}
// Log the supported routes
for _, mount := range handler.Mounts {
log.Printf("%q mounted on %s %s", mount.Method, mount.Verb, mount.Pattern)
}
// Start the server (this will block the execution)
log.Printf("Starting concerts service on :%s", port)
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
genconcerts.MakeNotFound()
for consistent error responsesMake sure to add the required dependencies to your go.mod
:
go mod tidy
This will automatically add:
goa.design/goa/v3
- Core Goa frameworkgithub.com/google/uuid
- UUID generationgoa gen concerts/design
go run cmd/concerts/main.go
You should see output similar to:
"List" mounted on GET /concerts
"Create" mounted on POST /concerts
"Show" mounted on GET /concerts/{concertID}
"Update" mounted on PUT /concerts/{concertID}
"Delete" mounted on DELETE /concerts/{concertID}
Starting concerts service on :8080
# List concerts (empty initially)
curl http://localhost:8080/concerts
# Create a new concert
curl -X POST "http://localhost:8080/concerts" \
-H "Content-Type: application/json" \
-d '{
"artist": "The White Stripes",
"date": "2024-12-25",
"venue": "Madison Square Garden, New York, NY",
"price": 7500
}'
Congratulations! 🎉 You’ve successfully implemented your first Goa service. The service now handles all CRUD operations with proper validation, error handling, and HTTP status codes. Head over to Running where we’ll explore different ways to interact with your service and test all the endpoints!