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/concerts.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:
openapi2.yaml
and openapi2.json
(Swagger)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 upcoming concerts with optional pagination.
List(context.Context, *ListPayload) (res []*Concert, err error)
// Create a new concert entry.
Create(context.Context, *ConcertPayload) (res *Concert, err error)
// Get a single concert by ID.
Show(context.Context, *ShowPayload) (res *Concert, err error)
// Update an existing concert by ID.
Update(context.Context, *UpdatePayload) (res *Concert, err error)
// Remove a concert from the system by ID.
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.ConcertPayloadCreatePayload) (*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)
}
}
go run concerts/cmd/concerts
curl http://localhost:8080/concerts
Congratulations! 🎉 You’ve successfully implemented your first Goa service. Now it’s time for the exciting part - seeing your API in action! Head over to Running where we’ll explore different ways to interact with your service and watch it handle real HTTP requests.