Learn how to define Treaty contracts, including request and response definitions, and service delegation. This guide covers the complete structure of a contract and best practices for organizing your API definitions.
A Treaty contract consists of:
- Class definition - inheriting from
Treaty::Action::Base - Version blocks - one or more version definitions
- Request definition - what data comes in
- Response definition(s) - what data goes out
- Delegation - where to process the request
module Gate
module API
module Posts
class CreateTreaty < ApplicationTreaty
version 1 do
request do
object :post do
string :title
string :content
end
end
response 201 do
object :post do
string :id
string :title
string :content
time :created_at
end
end
delegate_to Posts::CreateService
end
end
end
end
endversion 1 do
# Version configuration
endversion 2, default: true do
# This version is used when no version specified
endversion 3 do
summary "Added support for post categories and tags"
deprecated(lambda do
Gem::Version.new(ENV.fetch("RELEASE_VERSION", "0.0.0")) >=
Gem::Version.new("4.0.0")
end)
# ... rest of definition
endrequest do
object :post do
string :title
string :content
end
endrequest do
object :post do
string :title
string :content
string :summary, :optional
end
object :filters do
string :category, :optional
array :tags, :optional do
string :_self
end
end
endrequest do
object :user do
string :email, format: :email
string :password, format: {
is: :password,
message: "Password must be 8-16 characters with digit, lowercase, and uppercase"
}
string :birth_date, :optional, format: :date
string :external_id, :optional, format: :uuid
end
endrequest do
object :post do
string :title
string :content
string :status, in: %w[draft published]
# Published date only for published posts
datetime :published_at, :optional,
if: ->(post:) { post[:status] == "published" }
# Draft notes only for draft posts
string :draft_notes, :optional,
unless: ->(post:) { post[:status] == "published" }
end
endrequest do
object :post do
string :title
string :content
# Computed: derive slug from title
string :slug, :optional,
computed: ->(**attributes) { attributes.dig(:post, :title).to_s.downcase.gsub(/\s+/, "-") }
# Computed: calculate word count from content
integer :word_count, :optional,
computed: ->(**attributes) { attributes.dig(:post, :content).to_s.split.size }
object :author do
string :first_name
string :last_name
# Computed: combine first and last name
string :full_name, :optional,
computed: (lambda do |**attributes|
"#{attributes.dig(:post, :author, :first_name)} #{attributes.dig(:post, :author, :last_name)}"
end)
end
end
endrequest do
# These attributes go to root level
object :_self do
string :signature
string :timestamp
end
# These go under 'post' key
object :post do
string :title
end
end
# Expects data like:
# {
# signature: "abc123",
# timestamp: "2024-01-01",
# post: { title: "Hello" }
# }request do
object :post
object :filters
end
# Just declares that these objects existresponse 200 do
array :posts do
string :id
string :title
end
endresponse 200 do
array :posts do
string :id
string :title
end
object :meta do
integer :count
integer :page
end
end
response 201 do
object :post do
string :id
string :title
time :created_at
end
end
response 422 do
object :errors do
string :message
end
end# Constant
delegate_to Posts::CreateService
# String (constant)
delegate_to "Posts::CreateService"
# String (path, will be constantized)
delegate_to "posts/create_service"delegate_to(lambda do |params:|
# Process request directly
post = Post.create!(params[:post])
{ post: post.as_json }
end)# Specify method and return processing
delegate_to Posts::CreateService => :call, return: lambda(&:data)
# Service will be called as:
# service = Posts::CreateService.call(params: validated_params)
# result = return_lambda.call(service)Provide controller-specific data to services:
class PostsController < ApplicationController
treaty :index do
provide :current_user
provide :posts, from: :load_posts
provide :meta, from: -> { { timestamp: Time.current } }
end
private
def load_posts
Post.where(user: current_user).published
end
endServices receive inventory as a parameter:
class Posts::IndexService
def self.call(inventory:, params:)
current_user = inventory.current_user
posts = inventory.posts
# ...
end
endSee Inventory System for detailed documentation.
You can define multiple request blocks that will be merged:
request do
# Query parameters
object :_self do
string :signature
end
end
request do
# Body parameters
object :post do
string :title
string :content
end
end# Good
class Posts::CreateTreaty < ApplicationTreaty
# Handles posts#create
end
class Posts::UpdateTreaty < ApplicationTreaty
# Handles posts#update
end
# Bad - don't handle multiple actions in one treaty# Good
version 1 do
summary "Initial release"
end
version 2 do
summary "Added author support"
end
version 3 do
summary "Added categories and tags"
end
# Bad - no context about changes
version 1 do; end
version 2 do; endversion 3, default: true do
# This is the current production version
endversion 1 do
summary "Initial release - DEPRECATED"
deprecated(lambda do
# Will be removed in version 4.0.0
Gem::Version.new(ENV["RELEASE_VERSION"]) >= Gem::Version.new("4.0.0")
end)
# ... rest of definition
endHere's a complete example from the sandbox:
module Gate
module API
module Posts
class CreateTreaty < ApplicationTreaty
version 3 do
summary "Added author and socials to expand post data"
request do
object :_self do
string :signature
end
end
request do
object :post do
string :title
string :summary
string :description, :optional
string :content
array :tags, :optional do
string :_self
end
object :author do
string :name
string :bio
array :socials, :optional do
string :provider, in: %w[twitter linkedin github]
string :handle, as: :value
end
end
end
end
response 201 do
object :post do
string :id
string :title
string :summary
string :description
string :content
array :tags do
string :_self
end
object :author do
string :name
string :bio
array :socials do
string :provider
string :value, as: :handle
end
end
integer :rating
integer :views
time :created_at
time :updated_at
end
end
delegate_to "posts/stable/create_service"
end
end
end
end
end- Attributes - Learn about attribute types and options
- Objects - Understand object organization
- Versioning - Manage multiple API versions