Documentation Index Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt
Use this file to discover all available pages before exploring further.
In this tutorial, youβll build a notes service that exposes both REST and JSON-RPC endpoints, with automatic OpenAPI documentation. The magic of contracts is that you write your business logic once, and it works through multiple protocols automatically - no duplicate code.
What Weβre Building
A notes API with:
Create, list, get, update, delete operations
REST endpoints at /api/notes/*
JSON-RPC endpoint at /
OpenAPI spec at /openapi.json
Step 1: Create the Project
mizu new notes --template contract
cd notes
go mod tidy
Verify it works:
# Test the default todo service
curl http://localhost:8080/api/todo
Step 2: Create the Notes Service
Create the service directory:
Create service/notes/notes.go:
package notes
import (
" context "
" fmt "
" sync "
" time "
)
// Service is the notes service.
type Service struct {
mu sync . RWMutex
notes map [ string ] * Note
nextID int
}
// New creates a new notes service.
func New () * Service {
return & Service {
notes : make ( map [ string ] * Note ),
}
}
// Note represents a note.
type Note struct {
ID string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreatedAt time . Time `json:"createdAt"`
UpdatedAt time . Time `json:"updatedAt"`
}
// CreateInput is the input for creating a note.
type CreateInput struct {
Title string `json:"title"`
Content string `json:"content"`
}
// UpdateInput is the input for updating a note.
type UpdateInput struct {
ID string `json:"id"`
Title * string `json:"title,omitempty"`
Content * string `json:"content,omitempty"`
}
// GetInput is the input for getting a note.
type GetInput struct {
ID string `json:"id"`
}
// DeleteInput is the input for deleting a note.
type DeleteInput struct {
ID string `json:"id"`
}
// DeleteOutput is the output for deleting a note.
type DeleteOutput struct {
Success bool `json:"success"`
}
Step 3: Implement the Methods
Add the methods to service/notes/notes.go:
// Create creates a new note.
func ( s * Service ) Create ( ctx context . Context , in * CreateInput ) ( * Note , error ) {
if in . Title == "" {
return nil , fmt . Errorf ( "title is required" )
}
s . mu . Lock ()
defer s . mu . Unlock ()
s . nextID ++
now := time . Now ()
note := & Note {
ID : fmt . Sprintf ( " %d " , s . nextID ),
Title : in . Title ,
Content : in . Content ,
CreatedAt : now ,
UpdatedAt : now ,
}
s . notes [ note . ID ] = note
return note , nil
}
// List returns all notes.
func ( s * Service ) List ( ctx context . Context ) ([] * Note , error ) {
s . mu . RLock ()
defer s . mu . RUnlock ()
notes := make ([] * Note , 0 , len ( s . notes ))
for _ , n := range s . notes {
notes = append ( notes , n )
}
return notes , nil
}
// Get returns a note by ID.
func ( s * Service ) Get ( ctx context . Context , in * GetInput ) ( * Note , error ) {
s . mu . RLock ()
defer s . mu . RUnlock ()
note , ok := s . notes [ in . ID ]
if ! ok {
return nil , fmt . Errorf ( "note not found: %s " , in . ID )
}
return note , nil
}
// Update updates a note.
func ( s * Service ) Update ( ctx context . Context , in * UpdateInput ) ( * Note , error ) {
s . mu . Lock ()
defer s . mu . Unlock ()
note , ok := s . notes [ in . ID ]
if ! ok {
return nil , fmt . Errorf ( "note not found: %s " , in . ID )
}
if in . Title != nil {
note . Title = * in . Title
}
if in . Content != nil {
note . Content = * in . Content
}
note . UpdatedAt = time . Now ()
return note , nil
}
// Delete deletes a note.
func ( s * Service ) Delete ( ctx context . Context , in * DeleteInput ) ( * DeleteOutput , error ) {
s . mu . Lock ()
defer s . mu . Unlock ()
if _ , ok := s . notes [ in . ID ]; ! ok {
return nil , fmt . Errorf ( "note not found: %s " , in . ID )
}
delete ( s . notes , in . ID )
return & DeleteOutput { Success : true }, nil
}
Step 4: Register the Service
Update app/server/server.go:
package server
import (
" github.com/go-mizu/mizu "
" github.com/go-mizu/mizu/contract "
" example.com/notes/service/notes " // Add this
" example.com/notes/service/todo "
)
type Server struct {
cfg Config
app * mizu . App
registry * contract . Registry
}
func New ( cfg Config ) * Server {
s := & Server { cfg : cfg }
s . app = mizu . New ()
s . registry = contract . NewRegistry ()
// Register services
s . registry . Register ( "todo" , todo . New ())
s . registry . Register ( "notes" , notes . New ()) // Add this
s . setupTransports ()
return s
}
func ( s * Server ) setupTransports () {
// REST transport
rest := contract . NewREST ( s . registry )
s . app . Mount ( "/api" , rest . Handler ())
// JSON-RPC transport
rpc := contract . NewJSONRPC ( s . registry )
s . app . Post ( "/" , rpc . Handler ())
// OpenAPI spec
openapi := contract . NewOpenAPI ( s . registry , contract . OpenAPIOptions {
Title : "Notes API" ,
Description : "A simple notes service" ,
Version : "1.0.0" ,
})
s . app . Get ( "/openapi.json" , openapi . Handler ())
}
func ( s * Server ) Listen ( addr string ) error {
return s . app . Listen ( addr )
}
Step 5: Test the API
Restart the server:
Test via REST
# Create a note
curl -X POST http://localhost:8080/api/notes \
-H "Content-Type: application/json" \
-d '{"title":"My First Note","content":"Hello world!"}'
{ "id" : "1" , "title" : "My First Note" , "content" : "Hello world!" , "createdAt" : "2024-01-15T10:30:00Z" , "updatedAt" : "2024-01-15T10:30:00Z" }
# List notes
curl http://localhost:8080/api/notes
# Get a note
curl http://localhost:8080/api/notes/1
# Update a note
curl -X PUT http://localhost:8080/api/notes/1 \
-H "Content-Type: application/json" \
-d '{"title":"Updated Title"}'
# Delete a note
curl -X DELETE http://localhost:8080/api/notes/1
Test via JSON-RPC
# Create via JSON-RPC
curl -X POST http://localhost:8080 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "notes.Create",
"params": {"title": "RPC Note", "content": "Created via JSON-RPC"},
"id": 1
}'
{ "jsonrpc" : "2.0" , "result" :{ "id" : "2" , "title" : "RPC Note" , "content" : "Created via JSON-RPC" , "createdAt" : "..." , "updatedAt" : "..." }, "id" : 1 }
# List via JSON-RPC
curl -X POST http://localhost:8080 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"notes.List","id":2}'
Test via CLI
# List available methods
mizu contract ls
# Show method details
mizu contract show notes.Create
# Call a method
mizu contract call notes.Create '{"title":"CLI Note","content":"From CLI"}'
# List all notes
mizu contract call notes.List
View OpenAPI Spec
curl http://localhost:8080/openapi.json | jq .
Or open http://localhost:8080/openapi.json in your browser.
Step 6: Add Search Method
Letβs add a search capability. Add to service/notes/notes.go:
// SearchInput is the input for searching notes.
type SearchInput struct {
Query string `json:"query"`
}
// Search searches notes by title or content.
func ( s * Service ) Search ( ctx context . Context , in * SearchInput ) ([] * Note , error ) {
s . mu . RLock ()
defer s . mu . RUnlock ()
if in . Query == "" {
return [] * Note {}, nil
}
query := strings . ToLower ( in . Query )
var results [] * Note
for _ , n := range s . notes {
if strings . Contains ( strings . ToLower ( n . Title ), query ) ||
strings . Contains ( strings . ToLower ( n . Content ), query ) {
results = append ( results , n )
}
}
return results , nil
}
Add the import at the top:
import (
" context "
" fmt "
" strings " // Add this
" sync "
" time "
)
Test it:
# Via REST (custom method becomes POST)
curl -X POST http://localhost:8080/api/notes/search \
-H "Content-Type: application/json" \
-d '{"query":"hello"}'
# Via JSON-RPC
curl -X POST http://localhost:8080 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"notes.Search","params":{"query":"hello"},"id":1}'
# Via CLI
mizu contract call notes.Search '{"query":"hello"}'
What You Learned
Service contracts - Define business logic as Go methods
Transport-neutral - Same code works for REST and JSON-RPC
Auto-discovery - Methods are automatically exposed
OpenAPI generation - Documentation from your code
CLI integration - Test services from command line
Key Takeaways
Separation of concerns - Business logic in services, transport in setup
Type safety - Strongly typed inputs and outputs
Consistency - Same API via multiple protocols
Discoverability - Clients can explore your API
Next Steps
Contract Concepts Learn more about the contract system
Web Template Build server-rendered web apps