Addressbook Tutorial
Build a complete contact management API with multiple related resources, demonstrating real-world patterns and best practices.
Level: Intermediate | Time: 30 minutes
What You'll Learn
- Working with multiple related resources
- Using
$refto reference other schemas - Embedding resources with nested objects
- Adding WebSocket support with AsyncAPI
- Query parameters and filtering
- Security schemes for protected endpoints
- Building a complete working application
The Application
We'll build an addressbook system with three resources:
- Persons - People in your contacts
- Addressbook - Physical addresses where people live
- Postal Codes - Postal/ZIP code lookup
Key Features:
- Addresses can embed person objects
- Addresses can reference postal codes
- WebSocket support for real-time updates
- JWT authentication on write operations
- Query filtering by city, name, etc.
Prerequisites
# Ensure firestone is installed
poetry add firestoned
# Install openapi-generator for client generation
# macOS: brew install openapi-generator
# Or download from: https://openapi-generator.tech/
Project Structure
Create your project directory:
mkdir addressbook-api
cd addressbook-api
mkdir resources
Step 1: Define the Person Resource
Start with the person resource - the simplest of the three.
Create resources/person.yaml:
kind: persons
apiVersion: v1
versionInPath: false
metadata:
description: An example of a persons resource
# Pagination parameters available on all GET requests
default_query_params:
- name: limit
description: Limit the number of responses back
in: query
schema:
type: integer
- name: offset
description: The offset to start returning resources
in: query
schema:
type: integer
# Custom descriptions for each operation
descriptions:
resource:
get: List all persons in this collection
head: Determine the existence and size of persons in this collection
patch: Patch one or more persons in this collection
post: Create a new person in this collection, a new UUID key will be created
instance:
get: Get a specific person from this collection
head: Determine the existence and size of this person
patch: Patch this person in the collection
put: Put a new person in this collection, with the given UUID key
# Define which HTTP methods to expose
methods:
resource:
- get
- post
instance:
- delete
- get
- head
- put
instance_attrs: [] # No attribute-level operations
# Security: require JWT for write operations
security:
scheme:
bearer_auth:
scheme: bearer
type: http
bearerFormat: JWT
resource:
- post
instance:
- delete
- put
instance_attrs: []
# The schema definition
schema:
type: array
# UUID as unique identifier
key:
name: uuid
description: A UUID associated to this person
schema:
type: string
# Query parameters for filtering
query_params:
- name: last_name
description: Filter by last name
schema:
type: string
methods:
- get
# Person properties
items:
type: object
properties:
uuid:
expose: false # Don't expose as URL parameter
description: A UUID associated to this person
schema:
type: string
first_name:
description: The person's first name
type: string
last_name:
description: The person's last name
type: string
age:
description: The person's age
type: integer
hobbies:
description: The person's hobbies
type: array
items:
type: string
Key Points:
expose: falseon theuuidfield prevents it from being a URL parameter (it's already in the path as{uuid})securitysection requires JWT tokens for POST, PUT, and DELETEquery_paramsallows filtering:GET /persons?last_name=Smithdefault_query_paramsadds pagination to all GET requests
Step 2: Define the Postal Code Resource
Create resources/postal_codes.yaml:
kind: postal_codes
apiVersion: v1
metadata:
description: An example of a postal code resource
versionInPath: false
default_query_params:
- name: limit
description: Limit the number of responses back
in: query
schema:
type: integer
- name: offset
description: The offset to start returning resources
in: query
schema:
type: integer
descriptions:
resource:
get: List all postal codes in this collection
head: Determine the existence and size of postal codes in this collection
patch: Patch one or more postal codes in this collection
post: Create a new postal code in this collection
instance:
get: Get a specific postal code from this collection
head: Determine the existence and size of this postal code
patch: Patch this postal code in the collection
put: Put a new postal code in this collection
methods:
resource:
- get
- post
instance:
- delete
- get
- head
instance_attrs:
- delete
- get
security:
scheme:
bearer_auth:
scheme: bearer
type: http
bearerFormat: JWT
resource:
- post
instance:
- delete
- put
instance_attrs:
- delete
- put
schema:
type: array
key:
name: uuid
description: A UUID associated to this postal code
schema:
type: string
query_params:
- name: name
description: Filter by postal code name
schema:
type: string
methods:
- get
items:
type: object
properties:
uuid:
expose: false
description: A UUID associated to this postal code
schema:
type: string
name:
description: The postal code's name/id (e.g., "90210", "SW1A 1AA")
type: string
Step 3: Define the Addressbook Resource
Now the main resource that ties everything together.
Create resources/addressbook.yaml:
kind: addressbook
apiVersion: v1
metadata:
description: An example of an addressbook resource
versionInPath: false
default_query_params:
- name: limit
description: Limit the number of responses back
in: query
schema:
type: integer
- name: offset
description: The offset to start returning resources
in: query
schema:
type: integer
# Add WebSocket support via AsyncAPI
asyncapi:
servers:
dev:
url: ws://localhost
protocol: ws
description: The development websocket server
channels:
resources: true # WebSocket channel for /addressbook
instances: true # WebSocket channel for /addressbook/{id}
instance_attrs: true # WebSocket channel for /addressbook/{id}/{attr}
descriptions:
resource:
get: List all addresses in this addressbook
head: Determine the existence and size of addresses in this addressbook
patch: Patch one or more addresses in this addressbook
post: Create a new address in this addressbook, a new address key will be created
delete: Delete all addresses from this addressbook
instance:
get: Get a specific address from this addressbook
head: Determine the existence and size of this address
patch: Patch this address in the addressbook
put: Update an existing address in this addressbook
delete: Delete an address from this addressbook
methods:
resource:
- get
- post
instance:
- delete
- get
- head
- put
instance_attrs:
- delete
- get
- head
- put
schema:
type: array
key:
name: address_key
description: A unique identifier for an addressbook entry
schema:
type: string
# Query parameters
query_params:
- name: city
description: Filter by city name
required: false
schema:
type: string
methods:
- get
# Address properties
items:
type: object
properties:
address_key:
expose: false
description: A unique identifier for an addressbook entry
schema:
type: string
# Embedded person object - references person.yaml schema
person:
description: This is a person object that lives at this address
schema:
$ref: "person.yaml#/schema"
addrtype:
description: The address type, e.g. work or home
type: string
enum:
- work
- home
street:
description: The street and civic number of this address
type: string
city:
description: The city of this address
type: string
state:
description: The state of this address
type: string
country:
description: The country of this address
type: string
people:
description: A list of people's names living there
type: array
items:
type: string
is_valid:
description: Address is valid or not
type: boolean
required:
- addrtype
- street
- city
- state
- country
Key Features:
$ref: "person.yaml#/schema"- Embeds the person schema definitionasyncapisection - Enables WebSocket channels for real-time updatesenumon addrtype - Restricts values to "work" or "home"peoplearray - Demonstrates array propertiesis_validboolean - Shows boolean fields
Step 4: Generate OpenAPI Specification
Generate a unified OpenAPI spec from all three resources:
firestone generate \
--title "Addressbook API" \
--description "Contact management system with addresses, persons, and postal codes" \
--version "1.0.0" \
--resources resources/addressbook.yaml,resources/person.yaml,resources/postal_codes.yaml \
openapi > openapi.yaml
What was generated:
The OpenAPI spec includes:
- 9 paths (3 resources × 3 path types: collection, instance, instance_attrs)
- Security schemes (bearer_auth)
- Component schemas for all models
- Query parameters on GET endpoints
- Proper request/response bodies
View the spec:
cat openapi.yaml
You'll see paths like:
GET /persons- List personsPOST /persons- Create person (requires JWT)GET /persons/{uuid}- Get personPUT /persons/{uuid}- Update person (requires JWT)GET /addressbook- List addressesGET /addressbook?city=Seattle- Filter by city- And more...
Step 5: Generate AsyncAPI Specification
Generate the AsyncAPI spec for WebSocket support:
firestone generate \
--title "Addressbook API" \
--description "Real-time contact management" \
--version "1.0.0" \
--resources resources/addressbook.yaml \
asyncapi > asyncapi.yaml
Note: Only the addressbook resource has AsyncAPI configured.
View the spec:
cat asyncapi.yaml
You'll see:
- WebSocket server configuration (ws://localhost)
- Channels for /addressbook, /addressbook/{address_key}, etc.
- Message schemas for subscribe and publish operations
Step 6: Test with Swagger UI
Firestone includes a built-in Swagger UI server:
firestone generate \
--title "Addressbook API" \
--description "Contact management system" \
--version "1.0.0" \
--resources resources/addressbook.yaml,resources/person.yaml,resources/postal_codes.yaml \
openapi \
--ui-server
Output:
* Serving Flask app 'firestone_lib.ui_server'
* Running on http://127.0.0.1:5000
Open your browser to:
http://127.0.0.1:5000/apidocs
Try it out:
- Click on
GET /persons - Click "Try it out"
- Click "Execute"
- See the response format
You'll notice that POST, PUT, and DELETE have a lock icon indicating they require authentication.
Press Ctrl+C to stop the server.
Step 7: Generate Python Client
Use OpenAPI Generator to create a Python client SDK:
openapi-generator generate \
-i openapi.yaml \
-g python \
-o ./addressbook-client \
--package-name addressbook_client \
--additional-properties=packageVersion=1.0.0
This creates a complete Python client in ./addressbook-client/ with:
- API classes (
PersonsApi,AddressbookApi,PostalCodesApi) - Model classes (
Person,Addressbook,PostalCode) - Configuration and authentication support
- Complete documentation
Using the client:
from addressbook_client import ApiClient, Configuration
from addressbook_client.api import persons_api
from addressbook_client.models import CreatePerson
# Configure client
config = Configuration(host="http://localhost:8080")
config.access_token = "your-jwt-token"
with ApiClient(config) as api_client:
# Create API instance
api = persons_api.PersonsApi(api_client)
# List all persons
persons = api.get_persons()
# Filter by last name
smiths = api.get_persons(last_name="Smith")
# Get specific person
person = api.get_persons_uuid(uuid="123e4567-e89b-12d3-a456-426614174000")
# Create new person
new_person = CreatePerson(
first_name="John",
last_name="Doe",
age=30,
hobbies=["reading", "cycling"]
)
created = api.post_persons(create_person=new_person)
Step 8: Generate CLI Tool
Create a command-line interface:
firestone generate \
--title "Addressbook CLI" \
--description "Command-line interface for the Addressbook API" \
--version "1.0.0" \
--resources resources/addressbook.yaml,resources/person.yaml,resources/postal_codes.yaml \
cli \
--pkg addressbook \
--client-pkg addressbook_client > addressbook-cli.py
chmod +x addressbook-cli.py
Using the CLI:
# View help
python addressbook-cli.py --help
# List persons
python addressbook-cli.py persons list
# Filter persons
python addressbook-cli.py persons list --last-name Smith
# Get specific person
python addressbook-cli.py persons get 123e4567-e89b-12d3-a456-426614174000
# Create person (requires API_KEY env var)
export API_KEY="your-jwt-token"
python addressbook-cli.py persons create \
--first-name John \
--last-name Doe \
--age 30
# List addresses
python addressbook-cli.py addressbook list
# Filter by city
python addressbook-cli.py addressbook list --city Seattle
# List postal codes
python addressbook-cli.py postal-codes list
CLI Options:
All commands support:
--api-url- API base URL (default: http://localhost:8080)--api-key- JWT token for authentication--debug- Enable debug logging
Step 9: Implement a FastAPI Server
Let's create a simple server implementation. Create server.py:
#!/usr/bin/env python3
"""
Simple FastAPI server implementing the Addressbook API.
"""
from typing import List, Optional
from uuid import uuid4
from fastapi import FastAPI, HTTPException, Query, Header
from pydantic import BaseModel
app = FastAPI(
title="Addressbook API",
description="Contact management system",
version="1.0.0"
)
# In-memory storage (use a database in production!)
persons_db = {}
addresses_db = {}
postal_codes_db = {}
# Models (simplified - use generated models in production)
class Person(BaseModel):
uuid: str
first_name: str
last_name: str
age: Optional[int] = None
hobbies: List[str] = []
class CreatePerson(BaseModel):
first_name: str
last_name: str
age: Optional[int] = None
hobbies: List[str] = []
class Address(BaseModel):
address_key: str
addrtype: str # "work" or "home"
street: str
city: str
state: str
country: str
people: List[str] = []
is_valid: bool = True
person: Optional[Person] = None
class CreateAddress(BaseModel):
addrtype: str
street: str
city: str
state: str
country: str
people: List[str] = []
is_valid: bool = True
# Persons endpoints
@app.get("/persons", response_model=List[Person])
def list_persons(
last_name: Optional[str] = None,
limit: int = Query(20),
offset: int = Query(0)
):
"""List all persons in this collection."""
persons = list(persons_db.values())
# Filter by last name if provided
if last_name:
persons = [p for p in persons if p.last_name == last_name]
# Apply pagination
return persons[offset:offset + limit]
@app.post("/persons", response_model=Person, status_code=201)
def create_person(
person: CreatePerson,
authorization: Optional[str] = Header(None)
):
"""Create a new person."""
# In production: validate JWT token from authorization header
if not authorization:
raise HTTPException(status_code=401, detail="Unauthorized")
person_id = str(uuid4())
new_person = Person(uuid=person_id, **person.dict())
persons_db[person_id] = new_person
return new_person
@app.get("/persons/{uuid}", response_model=Person)
def get_person(uuid: str):
"""Get a specific person."""
if uuid not in persons_db:
raise HTTPException(status_code=404, detail="Person not found")
return persons_db[uuid]
@app.put("/persons/{uuid}", response_model=Person)
def update_person(
uuid: str,
person: CreatePerson,
authorization: Optional[str] = Header(None)
):
"""Update a person."""
if not authorization:
raise HTTPException(status_code=401, detail="Unauthorized")
if uuid not in persons_db:
raise HTTPException(status_code=404, detail="Person not found")
updated_person = Person(uuid=uuid, **person.dict())
persons_db[uuid] = updated_person
return updated_person
@app.delete("/persons/{uuid}", status_code=204)
def delete_person(
uuid: str,
authorization: Optional[str] = Header(None)
):
"""Delete a person."""
if not authorization:
raise HTTPException(status_code=401, detail="Unauthorized")
if uuid not in persons_db:
raise HTTPException(status_code=404, detail="Person not found")
del persons_db[uuid]
return None
# Addressbook endpoints
@app.get("/addressbook", response_model=List[Address])
def list_addresses(
city: Optional[str] = None,
limit: int = Query(20),
offset: int = Query(0)
):
"""List all addresses."""
addresses = list(addresses_db.values())
if city:
addresses = [a for a in addresses if a.city == city]
return addresses[offset:offset + limit]
@app.post("/addressbook", response_model=Address, status_code=201)
def create_address(address: CreateAddress):
"""Create a new address."""
address_key = str(uuid4())
new_address = Address(address_key=address_key, **address.dict())
addresses_db[address_key] = new_address
return new_address
@app.get("/addressbook/{address_key}", response_model=Address)
def get_address(address_key: str):
"""Get a specific address."""
if address_key not in addresses_db:
raise HTTPException(status_code=404, detail="Address not found")
return addresses_db[address_key]
if __name__ == "__main__":
import uvicorn
# Add some test data
test_person = Person(
uuid="123e4567-e89b-12d3-a456-426614174000",
first_name="Jane",
last_name="Smith",
age=28,
hobbies=["reading", "hiking"]
)
persons_db[test_person.uuid] = test_person
test_address = Address(
address_key="addr-001",
addrtype="home",
street="123 Main St",
city="Seattle",
state="WA",
country="USA",
people=["Jane Smith"],
person=test_person
)
addresses_db[test_address.address_key] = test_address
uvicorn.run(app, host="0.0.0.0", port=8080)
Run the server:
# Install dependencies
pip install fastapi uvicorn
# Run server
python server.py
Test it:
# In another terminal
curl http://localhost:8080/persons
# Or use the CLI
python addressbook-cli.py --api-url http://localhost:8080 persons list
Understanding the Schema References
The addressbook resource uses $ref to embed the person schema:
person:
description: This is a person object that lives at this address
schema:
$ref: "person.yaml#/schema"
This means:
- The
personfield in an address contains the full person schema - The schema is loaded from
person.yamlat the/schemaJSON pointer - Firestone resolves this at generation time
- The OpenAPI spec includes the full nested structure
In the generated OpenAPI:
components:
schemas:
Addressbook:
properties:
person:
# Full person schema is embedded here
type: object
properties:
first_name:
type: string
last_name:
type: string
# ... etc
WebSocket Support Explained
The asyncapi section in addressbook.yaml enables WebSocket channels:
asyncapi:
servers:
dev:
url: ws://localhost
protocol: ws
channels:
resources: true # /addressbook
instances: true # /addressbook/{address_key}
instance_attrs: true # /addressbook/{address_key}/{attr}
This generates AsyncAPI definitions for:
- Subscribe to address updates
- Publish new addresses
- Real-time notifications when addresses change
Implementation Note: The AsyncAPI spec defines the contract; you'd implement the WebSocket server using libraries like FastAPI WebSockets, Socket.IO, or similar.
Security Implementation
The security scheme requires JWT tokens for write operations:
security:
scheme:
bearer_auth:
scheme: bearer
type: http
bearerFormat: JWT
resource:
- post
instance:
- delete
- put
In practice:
# Set token as environment variable
export API_KEY="eyJhbGciOiJIUzI1NiIs..."
# CLI automatically uses it
python addressbook-cli.py persons create --first-name John --last-name Doe
# Or provide explicitly
python addressbook-cli.py --api-key "eyJhbG..." persons create ...
In the generated client:
config = Configuration(host="http://localhost:8080")
config.access_token = "eyJhbGciOiJIUzI1NiIs..."
Complete File Structure
Your project should now look like:
addressbook-api/
├── resources/
│ ├── addressbook.yaml
│ ├── person.yaml
│ └── postal_codes.yaml
├── openapi.yaml # Generated
├── asyncapi.yaml # Generated
├── addressbook-cli.py # Generated
├── server.py # Implemented
└── addressbook-client/ # Generated
├── addressbook_client/
│ ├── api/
│ │ ├── persons_api.py
│ │ ├── addressbook_api.py
│ │ └── postal_codes_api.py
│ ├── models/
│ │ ├── person.py
│ │ ├── addressbook.py
│ │ └── postal_code.py
│ └── ...
├── docs/
├── setup.py
└── README.md
What You've Learned
You've built a real-world API with:
✅ Multiple related resources - Persons, addresses, postal codes
✅ Schema references - Using $ref to embed schemas
✅ WebSocket support - AsyncAPI for real-time updates
✅ Security schemes - JWT authentication on endpoints
✅ Query parameters - Filtering and pagination
✅ Complete workflow - From resource to working server
✅ Client SDK - Generated Python client
✅ CLI tool - Command-line interface
Next Steps
Enhance your addressbook:
- Add More Resources - Tags, notes, photos
- Implement Relationships - Link persons to multiple addresses
- Add Validation - Email format, phone numbers
- Deploy the Server - Docker, Kubernetes
- Add Tests - pytest for the API
- WebSocket Implementation - Real-time updates
- Database Integration - PostgreSQL, MongoDB
See Also
- Multi-Resource Guide - Working with resource dependencies
- Validation Patterns - Advanced schema validation
- Authentication Guide - Security best practices
- Full Workflow - Production deployment
Source Code
The complete addressbook example is available in the firestone repository:
# View the source
ls /home/brad/firestoned/firestone/examples/addressbook/
# Copy to experiment
cp -r /home/brad/firestoned/firestone/examples/addressbook/ ./my-addressbook