Skip to content

Commit c2ab110

Browse files
committed
Add concerts example API
This commit introduces a new example API for managing concerts, including CRUD operations for concert data. The implementation features a RESTful design with endpoints for listing, creating, showing, updating, and deleting concerts. The API is built using Goa, showcasing design-first development principles. Key changes: - Added `concerts` module with necessary Go files and structure. - Implemented endpoints and handlers for concert management. - Included OpenAPI specifications for API documentation. - Updated `common.mk` to conditionally skip example generation. - Updated `go.work` to include the new `concerts` module. This example serves as a practical demonstration of building a REST API with Goa, covering essential design patterns and best practices.
1 parent 308bf6d commit c2ab110

26 files changed

Lines changed: 5127 additions & 2 deletions

common.mk

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ lint:
6161

6262
gen:
6363
@echo GENERATING CODE...
64-
@goa gen "$(MODULE)/design" && \
65-
goa example "$(MODULE)/design"
64+
@goa gen "$(MODULE)/design"
65+
ifneq ($(SKIP_GOA_EXAMPLE),true)
66+
@goa example "$(MODULE)/design"
67+
endif
6668

6769
build:
6870
@go build "./cmd/$(APP)" && go build "./cmd/$(APP)-cli"

concerts/Makefile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#! /usr/bin/make
2+
#
3+
# Makefile for Goa concerts example
4+
5+
APP=concerts
6+
7+
# Skip goa example generation to preserve custom implementation
8+
SKIP_GOA_EXAMPLE=true
9+
10+
include ../common.mk

concerts/README.md

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# Concert Management API
2+
3+
A practical example of building a REST API with [Goa](https://goa.design/),
4+
covering design patterns, validation, error handling, and documentation
5+
generation.
6+
7+
> **📚 Tutorial Reference**: This example is featured in the official
8+
> [Goa REST API Tutorial](https://goa.design/docs/3-tutorials/1-rest-api/) on
9+
> the Goa documentation site. The tutorial provides step-by-step guidance for
10+
> building REST APIs with Goa's design-first approach.
11+
12+
> **⚠️ Example Code**: This is a simplified example for learning purposes. For
13+
> simplicity, the entire service implementation lives in the `main.go` file.
14+
> In practice, you would typically organize code into separate packages as
15+
> shown in the
16+
> [Multiple Services guide](https://goa.design/docs/6-advanced/2-multiple-services/).
17+
18+
## What's This All About?
19+
20+
This is a concert management system that demonstrates how to build a clean REST
21+
API. You can manage concerts, artists, venues, and ticket prices through
22+
well-structured endpoints. The key advantage is that everything is defined
23+
upfront in the design, and Goa generates all the boilerplate code for you.
24+
25+
What makes this approach useful:
26+
- Clean, well-structured endpoints that follow REST conventions
27+
- Validation that happens before your business logic runs
28+
- Consistent error responses across all endpoints
29+
- Pagination that handles large datasets properly
30+
- Auto-generated OpenAPI documentation that stays current
31+
- Strong typing that catches issues at compile time
32+
33+
## What You Can Do
34+
35+
The API covers the standard concert management operations:
36+
37+
**Concert Management**: List concerts with pagination, create new events, get
38+
details for specific concerts, update existing information, and remove
39+
concerts.
40+
41+
**Data Model**: Each concert includes a unique ID (auto-generated), artist or
42+
band name, date in ISO format, venue information, and ticket price stored in
43+
cents.
44+
45+
## Getting It Running
46+
47+
### What You'll Need
48+
- Go 1.24 or later
49+
- [Goa v3](https://goa.design/) framework
50+
51+
### Setup
52+
53+
Get the code and dependencies:
54+
```bash
55+
git clone <repository>
56+
cd concerts
57+
go mod tidy
58+
```
59+
60+
Generate the Goa code:
61+
```bash
62+
goa gen concerts/design
63+
```
64+
65+
Start the server:
66+
```bash
67+
go run cmd/concerts/main.go
68+
```
69+
70+
The API will be available at `http://localhost:8080`.
71+
72+
### Documentation
73+
74+
Once running, you can view the auto-generated documentation:
75+
- **OpenAPI JSON**: http://localhost:8080/openapi3.json
76+
- **OpenAPI YAML**: http://localhost:8080/openapi3.yaml
77+
78+
## Try It Out
79+
80+
### List Concerts
81+
82+
```bash
83+
# Get the first page of concerts
84+
curl "http://localhost:8080/concerts"
85+
86+
# Get page 2 with 5 results
87+
curl "http://localhost:8080/concerts?page=2&limit=5"
88+
```
89+
90+
### Create a Concert
91+
92+
```bash
93+
curl -X POST "http://localhost:8080/concerts" \
94+
-H "Content-Type: application/json" \
95+
-d '{
96+
"artist": "The White Stripes",
97+
"date": "2024-12-25",
98+
"venue": "Madison Square Garden, New York, NY",
99+
"price": 8500
100+
}'
101+
```
102+
103+
### Get Concert Details
104+
105+
```bash
106+
curl "http://localhost:8080/concerts/550e8400-e29b-41d4-a716-446655440000"
107+
```
108+
109+
### Update a Concert
110+
111+
You can update all fields or just specific ones:
112+
113+
```bash
114+
# Update multiple fields
115+
curl -X PUT "http://localhost:8080/concerts/550e8400-e29b-41d4-a716-446655440000" \
116+
-H "Content-Type: application/json" \
117+
-d '{
118+
"artist": "The White Stripes",
119+
"date": "2024-12-26",
120+
"venue": "Madison Square Garden, New York, NY",
121+
"price": 9000
122+
}'
123+
124+
# Update just the price
125+
curl -X PUT "http://localhost:8080/concerts/550e8400-e29b-41d4-a716-446655440000" \
126+
-H "Content-Type: application/json" \
127+
-d '{
128+
"price": 9500
129+
}'
130+
```
131+
132+
### Delete a Concert
133+
134+
```bash
135+
curl -X DELETE "http://localhost:8080/concerts/550e8400-e29b-41d4-a716-446655440000"
136+
```
137+
138+
## Design Details
139+
140+
### Type Structure
141+
142+
The API uses a layered type approach for flexible operations:
143+
144+
- **ConcertData**: Base type with all concert fields, no required constraints
145+
- **ConcertPayload**: For creation - extends base type and requires all fields
146+
- **UpdatePayload**: For updates - extends base type but only requires the concert ID
147+
- **Concert**: For responses - complete concert with ID and all details
148+
149+
This structure allows creation to require complete data while updates can be partial.
150+
151+
### Validation Rules
152+
153+
All validation is defined in the design layer:
154+
- Artist names: 1-200 characters
155+
- Dates: ISO 8601 format (YYYY-MM-DD)
156+
- Venues: 1-300 characters
157+
- Prices: Non-negative integers, maximum $1000 (stored as cents)
158+
- Pagination: Page ≥ 1, Limit 1-100
159+
160+
### Error Handling
161+
162+
The API returns consistent error responses:
163+
164+
```json
165+
{
166+
"message": "Concert with ID abc123 not found",
167+
"code": "not_found"
168+
}
169+
```
170+
171+
HTTP status codes follow standard conventions:
172+
- `200 OK` for successful reads and updates
173+
- `201 Created` for successful creation
174+
- `204 No Content` for successful deletion
175+
- `400 Bad Request` for validation errors
176+
- `404 Not Found` for missing resources
177+
178+
## Project Structure
179+
180+
```bash
181+
concerts/
182+
├── design/
183+
│ └── design.go # API design specification
184+
├── cmd/concerts/
185+
│ └── main.go # Service implementation
186+
├── gen/ # Generated code (don't modify)
187+
│ ├── concerts/ # Service interfaces and types
188+
│ └── http/ # HTTP transport layer
189+
├── go.mod # Dependencies
190+
└── README.md # This file
191+
```
192+
193+
> **Note**: This example keeps everything in a single `main.go` file for
194+
> simplicity. In real applications, you would typically organize the service
195+
> implementation into separate packages, with clean separation between business
196+
> logic, transport handlers, and service interfaces as demonstrated in the
197+
> [Multiple Services documentation](https://goa.design/docs/6-advanced/2-multiple-services/).
198+
199+
## Development
200+
201+
To modify the API, edit `design/design.go` and regenerate:
202+
203+
```bash
204+
goa gen concerts/design
205+
```
206+
207+
Goa will update all the generated code while preserving your service implementation.
208+
209+
## Key Benefits
210+
211+
1. **Design-first development**: API specification drives implementation
212+
2. **Centralized validation**: Rules defined once in the design
213+
3. **Type safety**: Compile-time error detection
214+
4. **Consistent error handling**: Standardized response format
215+
5. **Always-current documentation**: Generated from the same source as code
216+
6. **RESTful design**: Proper HTTP methods and resource modeling
217+
7. **Efficient pagination**: Handles large datasets appropriately
218+
8. **Clean separation**: Business logic separate from transport concerns
219+
220+
## Dependencies
221+
222+
- [Goa v3](https://goa.design/): Design framework for building APIs
223+
- [UUID](https://github.com/google/uuid): For generating unique identifiers
224+
225+
This example demonstrates Goa's design-first approach for educational purposes.

concerts/cmd/concerts/main.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"net/http"
8+
9+
"github.com/google/uuid"
10+
goahttp "goa.design/goa/v3/http"
11+
12+
// Use gen prefix for generated packages
13+
genconcerts "goa.design/examples/concerts/gen/concerts"
14+
genhttp "goa.design/examples/concerts/gen/http/concerts/server"
15+
)
16+
17+
// ConcertsService implements the genconcerts.Service interface
18+
type ConcertsService struct {
19+
concerts []*genconcerts.Concert // In-memory storage
20+
}
21+
22+
// List upcoming concerts with optional pagination.
23+
func (m *ConcertsService) List(ctx context.Context, p *genconcerts.ListPayload) ([]*genconcerts.Concert, error) {
24+
start := (p.Page - 1) * p.Limit
25+
end := start + p.Limit
26+
if end > len(m.concerts) {
27+
end = len(m.concerts)
28+
}
29+
return m.concerts[start:end], nil
30+
}
31+
32+
// Create a new concerts entry.
33+
func (m *ConcertsService) Create(ctx context.Context, p *genconcerts.ConcertPayload) (*genconcerts.Concert, error) {
34+
newConcert := &genconcerts.Concert{
35+
ID: uuid.New().String(),
36+
Artist: p.Artist,
37+
Date: p.Date,
38+
Venue: p.Venue,
39+
Price: p.Price,
40+
}
41+
m.concerts = append(m.concerts, newConcert)
42+
return newConcert, nil
43+
}
44+
45+
// Get a single concert by ID.
46+
func (m *ConcertsService) Show(ctx context.Context, p *genconcerts.ShowPayload) (*genconcerts.Concert, error) {
47+
for _, concert := range m.concerts {
48+
if concert.ID == p.ConcertID {
49+
return concert, nil
50+
}
51+
}
52+
// Use designed error
53+
return nil, genconcerts.MakeNotFound(fmt.Errorf("concert not found: %s", p.ConcertID))
54+
}
55+
56+
// Update an existing concert by ID.
57+
func (m *ConcertsService) Update(ctx context.Context, p *genconcerts.UpdatePayload) (*genconcerts.Concert, error) {
58+
for i, concert := range m.concerts {
59+
if concert.ID == p.ConcertID {
60+
if p.Artist != nil {
61+
concert.Artist = *p.Artist
62+
}
63+
if p.Date != nil {
64+
concert.Date = *p.Date
65+
}
66+
if p.Venue != nil {
67+
concert.Venue = *p.Venue
68+
}
69+
if p.Price != nil {
70+
concert.Price = *p.Price
71+
}
72+
m.concerts[i] = concert
73+
return concert, nil
74+
}
75+
}
76+
return nil, genconcerts.MakeNotFound(fmt.Errorf("concert not found: %s", p.ConcertID))
77+
}
78+
79+
// Remove a concert from the system by ID.
80+
func (m *ConcertsService) Delete(ctx context.Context, p *genconcerts.DeletePayload) error {
81+
for i, concert := range m.concerts {
82+
if concert.ID == p.ConcertID {
83+
m.concerts = append(m.concerts[:i], m.concerts[i+1:]...)
84+
return nil
85+
}
86+
}
87+
return genconcerts.MakeNotFound(fmt.Errorf("concert not found: %s", p.ConcertID))
88+
}
89+
90+
// main instantiates the service and starts the HTTP server.
91+
func main() {
92+
// Instantiate the service
93+
svc := &ConcertsService{}
94+
95+
// Wrap it in the generated endpoints
96+
endpoints := genconcerts.NewEndpoints(svc)
97+
98+
// Build an HTTP handler
99+
mux := goahttp.NewMuxer()
100+
requestDecoder := goahttp.RequestDecoder
101+
responseEncoder := goahttp.ResponseEncoder
102+
handler := genhttp.New(endpoints, mux, requestDecoder, responseEncoder, nil, nil)
103+
104+
// Mount the handler on the mux
105+
genhttp.Mount(mux, handler)
106+
107+
// Create a new HTTP server
108+
port := "8080"
109+
server := &http.Server{Addr: ":" + port, Handler: mux}
110+
111+
// Log the supported routes
112+
for _, mount := range handler.Mounts {
113+
log.Printf("%q mounted on %s %s", mount.Method, mount.Verb, mount.Pattern)
114+
}
115+
116+
// Start the server (this will block the execution)
117+
log.Printf("Starting concerts service on :%s", port)
118+
if err := server.ListenAndServe(); err != nil {
119+
log.Fatal(err)
120+
}
121+
}

0 commit comments

Comments
 (0)