Offset Pagination
Offset-based pagination with page number and page size query parameters
API Definition
config/apis/steady_horse.rb
rb
# frozen_string_literal: true
Apiwork::API.define '/steady_horse' do
key_format :camel
export :openapi
export :apiwork
resources :products
endModels
app/models/steady_horse/product.rb
rb
# frozen_string_literal: true
module SteadyHorse
class Product < ApplicationRecord
validates :name, :price, :category, presence: true
end
endDatabase Table
| Column | Type | Nullable | Default |
|---|---|---|---|
| id | string | ||
| category | string | ||
| created_at | datetime | ||
| name | string | ||
| price | decimal | ||
| updated_at | datetime |
Representations
app/representations/steady_horse/product_representation.rb
rb
# frozen_string_literal: true
module SteadyHorse
class ProductRepresentation < Apiwork::Representation::Base
attribute :id
attribute :name, filterable: true, writable: true
attribute :price, sortable: true, writable: true
attribute :category, filterable: true, writable: true
attribute :created_at, sortable: true
attribute :updated_at, sortable: true
end
endContracts
app/contracts/steady_horse/product_contract.rb
rb
# frozen_string_literal: true
module SteadyHorse
class ProductContract < Apiwork::Contract::Base
representation ProductRepresentation
end
endControllers
app/controllers/steady_horse/products_controller.rb
rb
# frozen_string_literal: true
module SteadyHorse
class ProductsController < ApplicationController
before_action :set_product, only: %i[show update destroy]
def index
products = Product.all
expose products
end
def show
expose product
end
def create
product = Product.create(contract.body[:product])
expose product
end
def update
product.update(contract.body[:product])
expose product
end
def destroy
product.destroy
expose product
end
private
attr_reader :product
def set_product
@product = Product.find(params[:id])
end
end
endRequest Examples
First page (default)
Request
http
GET /steady_horse/productsResponse 200
json
{
"products": [
{
"id": "267adb6a-d0b4-54a7-a389-3b98a9eaa28c",
"name": "Wireless Keyboard",
"price": "49.99",
"category": "electronics",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-01T12:00:00.000Z"
},
{
"id": "03df3eee-f5c2-5936-833e-1428b7014046",
"name": "USB-C Cable",
"price": "12.99",
"category": "electronics",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-01T12:00:00.000Z"
},
{
"id": "9b201354-c4da-5ac1-b1af-277760ef44ae",
"name": "Desk Lamp",
"price": "34.99",
"category": "office",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-01T12:00:00.000Z"
},
{
"id": "5ce18568-b578-5e99-b0fa-807d16a49d0f",
"name": "Notebook",
"price": "8.99",
"category": "office",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-01T12:00:00.000Z"
},
{
"id": "de39e4db-cbc3-5392-a6d0-467af1c5b29d",
"name": "Monitor Stand",
"price": "79.99",
"category": "office",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-01T12:00:00.000Z"
},
{
"id": "040ac486-238a-5942-abec-ad251c0cbb51",
"name": "Webcam",
"price": "59.99",
"category": "electronics",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-01T12:00:00.000Z"
},
{
"id": "cb8c0234-a0a3-589a-9dba-12e95296dc98",
"name": "Mouse Pad",
"price": "14.99",
"category": "accessories",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-01T12:00:00.000Z"
},
{
"id": "bf56b805-7bca-54ee-a90b-c9ebf84d9bec",
"name": "Headphones",
"price": "89.99",
"category": "electronics",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-01T12:00:00.000Z"
}
],
"pagination": {
"items": 8,
"total": 1,
"current": 1,
"next": null,
"prev": null
}
}Specific page
Request
http
GET /steady_horse/products?page[number]=2&page[size]=3Response 200
json
{
"products": [
{
"id": "5ce18568-b578-5e99-b0fa-807d16a49d0f",
"name": "Notebook",
"price": "8.99",
"category": "office",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-01T12:00:00.000Z"
},
{
"id": "de39e4db-cbc3-5392-a6d0-467af1c5b29d",
"name": "Monitor Stand",
"price": "79.99",
"category": "office",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-01T12:00:00.000Z"
},
{
"id": "040ac486-238a-5942-abec-ad251c0cbb51",
"name": "Webcam",
"price": "59.99",
"category": "electronics",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-01T12:00:00.000Z"
}
],
"pagination": {
"items": 8,
"total": 3,
"current": 2,
"next": 3,
"prev": 1
}
}Custom page size
Request
http
GET /steady_horse/products?page[size]=2Response 200
json
{
"products": [
{
"id": "267adb6a-d0b4-54a7-a389-3b98a9eaa28c",
"name": "Wireless Keyboard",
"price": "49.99",
"category": "electronics",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-01T12:00:00.000Z"
},
{
"id": "03df3eee-f5c2-5936-833e-1428b7014046",
"name": "USB-C Cable",
"price": "12.99",
"category": "electronics",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-01T12:00:00.000Z"
}
],
"pagination": {
"items": 5,
"total": 3,
"current": 1,
"next": 2,
"prev": null
}
}Exports
OpenAPI
yml
---
openapi: 3.1.0
info:
title: "/steady_horse"
version: 1.0.0
paths:
"/products":
get:
operationId: productsIndex
parameters:
- in: query
name: filter
required: false
schema:
oneOf:
- "$ref": "#/components/schemas/productFilter"
- items:
"$ref": "#/components/schemas/productFilter"
type: array
- in: query
name: page
required: false
schema:
"$ref": "#/components/schemas/productPage"
- in: query
name: sort
required: false
schema:
oneOf:
- "$ref": "#/components/schemas/productSort"
- items:
"$ref": "#/components/schemas/productSort"
type: array
responses:
'200':
content:
application/json:
schema:
properties:
meta:
properties: {}
type: object
pagination:
"$ref": "#/components/schemas/offsetPagination"
products:
items:
"$ref": "#/components/schemas/product"
type: array
type: object
required:
- pagination
- products
description: ''
post:
operationId: productsCreate
requestBody:
content:
application/json:
schema:
properties:
product:
"$ref": "#/components/schemas/productCreatePayload"
type: object
required:
- product
required: true
responses:
'200':
content:
application/json:
schema:
properties:
meta:
properties: {}
type: object
product:
"$ref": "#/components/schemas/product"
type: object
required:
- product
description: ''
'422':
description: Unprocessable Entity
content:
application/json:
schema:
properties:
issues:
items:
"$ref": "#/components/schemas/error"
type: array
required:
- issues
type: object
"/products/{id}":
get:
operationId: productsShow
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
'200':
content:
application/json:
schema:
properties:
meta:
properties: {}
type: object
product:
"$ref": "#/components/schemas/product"
type: object
required:
- product
description: ''
patch:
operationId: productsUpdate
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
properties:
product:
"$ref": "#/components/schemas/productUpdatePayload"
type: object
required:
- product
required: true
responses:
'200':
content:
application/json:
schema:
properties:
meta:
properties: {}
type: object
product:
"$ref": "#/components/schemas/product"
type: object
required:
- product
description: ''
'422':
description: Unprocessable Entity
content:
application/json:
schema:
properties:
issues:
items:
"$ref": "#/components/schemas/error"
type: array
required:
- issues
type: object
delete:
operationId: productsDestroy
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
'204':
description: ''
components:
schemas:
error:
properties:
issues:
items:
"$ref": "#/components/schemas/errorIssue"
type: array
layer:
enum:
- http
- contract
- domain
type: string
type: object
required:
- issues
- layer
errorIssue:
properties:
code:
type: string
detail:
type: string
meta:
properties: {}
type: object
path:
items:
oneOf:
- type: string
- type: integer
type: array
pointer:
type: string
type: object
required:
- code
- detail
- meta
- path
- pointer
offsetPagination:
properties:
current:
type: integer
items:
type: integer
next:
type:
- integer
- 'null'
prev:
type:
- integer
- 'null'
total:
type: integer
type: object
required:
- current
- items
- total
product:
properties:
category:
type: string
createdAt:
type: string
format: date-time
id:
type: string
name:
type: string
price:
type: number
updatedAt:
type: string
format: date-time
type: object
required:
- category
- createdAt
- id
- name
- price
- updatedAt
productCreatePayload:
properties:
category:
type: string
name:
type: string
price:
type: number
minimum: -99999999.99
maximum: 99999999.99
type: object
required:
- category
- name
- price
productFilter:
properties:
AND:
items:
"$ref": "#/components/schemas/productFilter"
type: array
NOT:
"$ref": "#/components/schemas/productFilter"
OR:
items:
"$ref": "#/components/schemas/productFilter"
type: array
category:
oneOf:
- type: string
- "$ref": "#/components/schemas/stringFilter"
name:
oneOf:
- type: string
- "$ref": "#/components/schemas/stringFilter"
type: object
productPage:
properties:
number:
type: integer
minimum: 1
size:
type: integer
minimum: 1
maximum: 100
type: object
productSort:
properties:
createdAt:
enum:
- asc
- desc
type: string
price:
enum:
- asc
- desc
type: string
updatedAt:
enum:
- asc
- desc
type: string
type: object
productUpdatePayload:
properties:
category:
type: string
name:
type: string
price:
type: number
minimum: -99999999.99
maximum: 99999999.99
type: object
stringFilter:
properties:
contains:
type: string
endsWith:
type: string
eq:
type: string
in:
items:
type: string
type: array
startsWith:
type: string
type: objectApiwork
json
{
"base_path": "/steady_horse",
"enums": [
{
"deprecated": false,
"description": null,
"example": null,
"name": "error_layer",
"scope": null,
"values": [
"http",
"contract",
"domain"
]
},
{
"deprecated": false,
"description": null,
"example": null,
"name": "sort_direction",
"scope": null,
"values": [
"asc",
"desc"
]
}
],
"error_codes": [
{
"description": "Unprocessable Entity",
"name": "unprocessable_entity",
"status": 422
}
],
"fingerprint": "f7693fa764c6617f",
"info": null,
"locales": [],
"resources": [
{
"actions": [
{
"name": "products.index",
"deprecated": false,
"description": null,
"method": "get",
"operation_id": null,
"path": "/products",
"raises": [],
"request": {
"body": [],
"description": null,
"query": [
{
"name": "filter",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "union",
"discriminator": "",
"variants": [
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "product_filter"
},
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "array",
"example": null,
"max": null,
"min": null,
"of": {
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "product_filter"
}
}
]
},
{
"name": "page",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "reference",
"reference": "product_page"
},
{
"name": "sort",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "union",
"discriminator": "",
"variants": [
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "product_sort"
},
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "array",
"example": null,
"max": null,
"min": null,
"of": {
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "product_sort"
}
}
]
}
]
},
"response": {
"body": {
"deprecated": null,
"description": null,
"nullable": null,
"optional": null,
"type": "object",
"partial": null,
"shape": [
{
"name": "meta",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "object",
"partial": false,
"shape": []
},
{
"name": "pagination",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "offset_pagination"
},
{
"name": "products",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "array",
"example": null,
"max": null,
"min": null,
"of": {
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "product"
}
}
]
},
"description": null,
"no_content": false
},
"summary": null,
"tags": []
},
{
"name": "products.show",
"deprecated": false,
"description": null,
"method": "get",
"operation_id": null,
"path": "/products/:id",
"raises": [],
"request": {
"body": [],
"description": null,
"query": []
},
"response": {
"body": {
"deprecated": null,
"description": null,
"nullable": null,
"optional": null,
"type": "object",
"partial": null,
"shape": [
{
"name": "meta",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "object",
"partial": false,
"shape": []
},
{
"name": "product",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "product"
}
]
},
"description": null,
"no_content": false
},
"summary": null,
"tags": []
},
{
"name": "products.create",
"deprecated": false,
"description": null,
"method": "post",
"operation_id": null,
"path": "/products",
"raises": [
"unprocessable_entity"
],
"request": {
"body": [
{
"name": "product",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "product_create_payload"
}
],
"description": null,
"query": []
},
"response": {
"body": {
"deprecated": null,
"description": null,
"nullable": null,
"optional": null,
"type": "object",
"partial": null,
"shape": [
{
"name": "meta",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "object",
"partial": false,
"shape": []
},
{
"name": "product",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "product"
}
]
},
"description": null,
"no_content": false
},
"summary": null,
"tags": []
},
{
"name": "products.update",
"deprecated": false,
"description": null,
"method": "patch",
"operation_id": null,
"path": "/products/:id",
"raises": [
"unprocessable_entity"
],
"request": {
"body": [
{
"name": "product",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "product_update_payload"
}
],
"description": null,
"query": []
},
"response": {
"body": {
"deprecated": null,
"description": null,
"nullable": null,
"optional": null,
"type": "object",
"partial": null,
"shape": [
{
"name": "meta",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "object",
"partial": false,
"shape": []
},
{
"name": "product",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "product"
}
]
},
"description": null,
"no_content": false
},
"summary": null,
"tags": []
},
{
"name": "products.destroy",
"deprecated": false,
"description": null,
"method": "delete",
"operation_id": null,
"path": "/products/:id",
"raises": [],
"request": {
"body": [],
"description": null,
"query": []
},
"response": {
"body": null,
"description": null,
"no_content": true
},
"summary": null,
"tags": []
}
],
"identifier": "products",
"name": "products",
"parent_identifiers": [],
"path": "products",
"resources": [],
"scope": "product"
}
],
"types": [
{
"recursive": true,
"deprecated": false,
"description": null,
"example": null,
"name": "product_filter",
"scope": "product",
"type": "object",
"extends": [],
"shape": [
{
"name": "AND",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "array",
"example": null,
"max": null,
"min": null,
"of": {
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "product_filter"
}
},
{
"name": "NOT",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "reference",
"reference": "product_filter"
},
{
"name": "OR",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "array",
"example": null,
"max": null,
"min": null,
"of": {
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "product_filter"
}
},
{
"name": "category",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "union",
"discriminator": "",
"variants": [
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "string_filter"
}
]
},
{
"name": "name",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "union",
"discriminator": "",
"variants": [
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "string_filter"
}
]
}
]
},
{
"recursive": false,
"deprecated": false,
"description": null,
"example": null,
"name": "error_issue",
"scope": null,
"type": "object",
"extends": [],
"shape": [
{
"name": "code",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "detail",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "meta",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "object",
"partial": false,
"shape": []
},
{
"name": "path",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "array",
"example": null,
"max": null,
"min": null,
"of": {
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "union",
"discriminator": "",
"variants": [
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "integer",
"example": null,
"format": null,
"max": null,
"min": null
}
]
}
},
{
"name": "pointer",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
}
]
},
{
"recursive": false,
"deprecated": false,
"description": null,
"example": null,
"name": "offset_pagination",
"scope": null,
"type": "object",
"extends": [],
"shape": [
{
"name": "current",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "integer",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "items",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "integer",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "next",
"deprecated": false,
"description": null,
"nullable": true,
"optional": true,
"type": "integer",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "prev",
"deprecated": false,
"description": null,
"nullable": true,
"optional": true,
"type": "integer",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "total",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "integer",
"example": null,
"format": null,
"max": null,
"min": null
}
]
},
{
"recursive": false,
"deprecated": false,
"description": null,
"example": null,
"name": "product",
"scope": "product",
"type": "object",
"extends": [],
"shape": [
{
"name": "category",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "createdAt",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "datetime",
"example": null
},
{
"name": "id",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "name",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "price",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "decimal",
"example": null,
"max": null,
"min": null
},
{
"name": "updatedAt",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "datetime",
"example": null
}
]
},
{
"recursive": false,
"deprecated": false,
"description": null,
"example": null,
"name": "product_create_payload",
"scope": "product",
"type": "object",
"extends": [],
"shape": [
{
"name": "category",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "name",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "price",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "decimal",
"example": null,
"max": 99999999.99,
"min": -99999999.99
}
]
},
{
"recursive": false,
"deprecated": false,
"description": null,
"example": null,
"name": "product_page",
"scope": "product",
"type": "object",
"extends": [],
"shape": [
{
"name": "number",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "integer",
"example": null,
"format": null,
"max": null,
"min": 1
},
{
"name": "size",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "integer",
"example": null,
"format": null,
"max": 100,
"min": 1
}
]
},
{
"recursive": false,
"deprecated": false,
"description": null,
"example": null,
"name": "product_sort",
"scope": "product",
"type": "object",
"extends": [],
"shape": [
{
"name": "createdAt",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "reference",
"reference": "sort_direction"
},
{
"name": "price",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "reference",
"reference": "sort_direction"
},
{
"name": "updatedAt",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "reference",
"reference": "sort_direction"
}
]
},
{
"recursive": false,
"deprecated": false,
"description": null,
"example": null,
"name": "product_update_payload",
"scope": "product",
"type": "object",
"extends": [],
"shape": [
{
"name": "category",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "name",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "price",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "decimal",
"example": null,
"max": 99999999.99,
"min": -99999999.99
}
]
},
{
"recursive": false,
"deprecated": false,
"description": null,
"example": null,
"name": "string_filter",
"scope": null,
"type": "object",
"extends": [],
"shape": [
{
"name": "contains",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "endsWith",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "eq",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
},
{
"name": "in",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "array",
"example": null,
"max": null,
"min": null,
"of": {
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
}
},
{
"name": "startsWith",
"deprecated": false,
"description": null,
"nullable": false,
"optional": true,
"type": "string",
"example": null,
"format": null,
"max": null,
"min": null
}
]
},
{
"recursive": false,
"deprecated": false,
"description": null,
"example": null,
"name": "error",
"scope": null,
"type": "object",
"extends": [],
"shape": [
{
"name": "issues",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "array",
"example": null,
"max": null,
"min": null,
"of": {
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "error_issue"
}
},
{
"name": "layer",
"deprecated": false,
"description": null,
"nullable": false,
"optional": false,
"type": "reference",
"reference": "error_layer"
}
]
}
]
}Codegen
TypeScript
ts
export type ErrorLayer = 'http' | 'contract' | 'domain';
export type SortDirection = 'asc' | 'desc';
export interface ErrorIssue {
code: string;
detail: string;
meta: Record<string, unknown>;
path: string | number[];
pointer: string;
}
export interface OffsetPagination {
current: number;
items: number;
next?: number | null;
prev?: number | null;
total: number;
}
export interface StringFilter {
contains?: string;
endsWith?: string;
eq?: string;
in?: string[];
startsWith?: string;
}
export interface Error {
issues: ErrorIssue[];
layer: ErrorLayer;
}ts
export * from './product';ts
import type { SortDirection, StringFilter } from '../api';
export interface ProductFilter {
AND?: ProductFilter[];
category?: string | StringFilter;
NOT?: ProductFilter;
name?: string | StringFilter;
OR?: ProductFilter[];
}
export interface Product {
category: string;
createdAt: string;
id: string;
name: string;
price: number;
updatedAt: string;
}
export interface ProductCreatePayload {
category: string;
name: string;
price: number;
}
export interface ProductPage {
number?: number;
size?: number;
}
export interface ProductSort {
createdAt?: SortDirection;
price?: SortDirection;
updatedAt?: SortDirection;
}
export interface ProductUpdatePayload {
category?: string;
name?: string;
price?: number;
}ts
export * from './products';ts
import type { OffsetPagination } from '../api';
import type {
Product,
ProductCreatePayload,
ProductFilter,
ProductPage,
ProductSort,
ProductUpdatePayload,
} from '../domains/product';
export type ProductsIndexMethod = 'GET';
export type ProductsIndexPath = '/products';
export interface ProductsIndexRequestQuery {
filter?: ProductFilter | ProductFilter[];
page?: ProductPage;
sort?: ProductSort | ProductSort[];
}
export type ProductsIndexResponseBody = {
meta?: Record<string, unknown>;
pagination: OffsetPagination;
products: Product[];
};
export interface ProductsIndexRequest {
query: ProductsIndexRequestQuery;
}
export interface ProductsIndexResponse {
body: ProductsIndexResponseBody;
}
export interface ProductsIndex {
method: ProductsIndexMethod;
path: ProductsIndexPath;
request: ProductsIndexRequest;
response: ProductsIndexResponse;
}
export type ProductsShowMethod = 'GET';
export type ProductsShowPath = '/products/:id';
export interface ProductsShowPathParams {
id: string;
}
export type ProductsShowResponseBody = {
meta?: Record<string, unknown>;
product: Product;
};
export interface ProductsShowResponse {
body: ProductsShowResponseBody;
}
export interface ProductsShow {
method: ProductsShowMethod;
path: ProductsShowPath;
pathParams: ProductsShowPathParams;
response: ProductsShowResponse;
}
export type ProductsCreateMethod = 'POST';
export type ProductsCreatePath = '/products';
export interface ProductsCreateRequestBody {
product: ProductCreatePayload;
}
export type ProductsCreateResponseBody = {
meta?: Record<string, unknown>;
product: Product;
};
export interface ProductsCreateRequest {
body: ProductsCreateRequestBody;
}
export interface ProductsCreateResponse {
body: ProductsCreateResponseBody;
}
export type ProductsCreateErrors = 422;
export interface ProductsCreate {
errors: ProductsCreateErrors;
method: ProductsCreateMethod;
path: ProductsCreatePath;
request: ProductsCreateRequest;
response: ProductsCreateResponse;
}
export type ProductsUpdateMethod = 'PATCH';
export type ProductsUpdatePath = '/products/:id';
export interface ProductsUpdatePathParams {
id: string;
}
export interface ProductsUpdateRequestBody {
product: ProductUpdatePayload;
}
export type ProductsUpdateResponseBody = {
meta?: Record<string, unknown>;
product: Product;
};
export interface ProductsUpdateRequest {
body: ProductsUpdateRequestBody;
}
export interface ProductsUpdateResponse {
body: ProductsUpdateResponseBody;
}
export type ProductsUpdateErrors = 422;
export interface ProductsUpdate {
errors: ProductsUpdateErrors;
method: ProductsUpdateMethod;
path: ProductsUpdatePath;
pathParams: ProductsUpdatePathParams;
request: ProductsUpdateRequest;
response: ProductsUpdateResponse;
}
export type ProductsDestroyMethod = 'DELETE';
export type ProductsDestroyPath = '/products/:id';
export interface ProductsDestroyPathParams {
id: string;
}
export interface ProductsDestroy {
method: ProductsDestroyMethod;
path: ProductsDestroyPath;
pathParams: ProductsDestroyPathParams;
}Zod
ts
import * as z from 'zod';
export const ErrorLayerSchema = z.enum(['contract', 'domain', 'http']);
export const SortDirectionSchema = z.enum(['asc', 'desc']);
export const ErrorIssueSchema = z.object({
code: z.string(),
detail: z.string(),
meta: z.record(z.string(), z.unknown()),
path: z.array(z.union([z.string(), z.number().int()])),
pointer: z.string(),
});
export const OffsetPaginationSchema = z.object({
current: z.number().int(),
items: z.number().int(),
next: z.number().int().nullable().optional(),
prev: z.number().int().nullable().optional(),
total: z.number().int(),
});
export const StringFilterSchema = z.object({
contains: z.string().optional(),
endsWith: z.string().optional(),
eq: z.string().optional(),
in: z.array(z.string()).optional(),
startsWith: z.string().optional(),
});
export const ErrorSchema = z.object({
issues: z.array(ErrorIssueSchema),
layer: ErrorLayerSchema,
});
export type ErrorLayer = 'contract' | 'domain' | 'http';
export type SortDirection = 'asc' | 'desc';
export interface ErrorIssue {
code: string;
detail: string;
meta: Record<string, unknown>;
path: string | number[];
pointer: string;
}
export interface OffsetPagination {
current: number;
items: number;
next?: number | null;
prev?: number | null;
total: number;
}
export interface StringFilter {
contains?: string;
endsWith?: string;
eq?: string;
in?: string[];
startsWith?: string;
}
export interface Error {
issues: ErrorIssue[];
layer: ErrorLayer;
}ts
export * from './product';ts
import type { SortDirection, StringFilter } from '../api';
import * as z from 'zod';
import { SortDirectionSchema, StringFilterSchema } from '../api';
export const ProductFilterSchema: z.ZodType<ProductFilter> = z.lazy(() =>
z.object({
AND: z.array(ProductFilterSchema).optional(),
category: z.union([z.string(), StringFilterSchema]).optional(),
NOT: ProductFilterSchema.optional(),
name: z.union([z.string(), StringFilterSchema]).optional(),
OR: z.array(ProductFilterSchema).optional(),
}),
);
export const ProductSchema = z.object({
category: z.string(),
createdAt: z.iso.datetime(),
id: z.string(),
name: z.string(),
price: z.number(),
updatedAt: z.iso.datetime(),
});
export const ProductCreatePayloadSchema = z.object({
category: z.string(),
name: z.string(),
price: z.number().min(-99999999.99).max(99999999.99),
});
export const ProductPageSchema = z.object({
number: z.number().int().min(1).optional(),
size: z.number().int().min(1).max(100).optional(),
});
export const ProductSortSchema = z.object({
createdAt: SortDirectionSchema.optional(),
price: SortDirectionSchema.optional(),
updatedAt: SortDirectionSchema.optional(),
});
export const ProductUpdatePayloadSchema = z.object({
category: z.string().optional(),
name: z.string().optional(),
price: z.number().min(-99999999.99).max(99999999.99).optional(),
});
export interface ProductFilter {
AND?: ProductFilter[];
category?: string | StringFilter;
NOT?: ProductFilter;
name?: string | StringFilter;
OR?: ProductFilter[];
}
export interface Product {
category: string;
createdAt: string;
id: string;
name: string;
price: number;
updatedAt: string;
}
export interface ProductCreatePayload {
category: string;
name: string;
price: number;
}
export interface ProductPage {
number?: number;
size?: number;
}
export interface ProductSort {
createdAt?: SortDirection;
price?: SortDirection;
updatedAt?: SortDirection;
}
export interface ProductUpdatePayload {
category?: string;
name?: string;
price?: number;
}ts
export * from './products';ts
import type { OffsetPagination } from '../api';
import type {
Product,
ProductCreatePayload,
ProductFilter,
ProductPage,
ProductSort,
ProductUpdatePayload,
} from '../domains/product';
import * as z from 'zod';
import { OffsetPaginationSchema } from '../api';
import {
ProductCreatePayloadSchema,
ProductFilterSchema,
ProductPageSchema,
ProductSchema,
ProductSortSchema,
ProductUpdatePayloadSchema,
} from '../domains/product';
export const ProductsIndexRequestQuerySchema = z.object({
filter: z
.union([ProductFilterSchema, z.array(ProductFilterSchema)])
.optional(),
page: ProductPageSchema.optional(),
sort: z.union([ProductSortSchema, z.array(ProductSortSchema)]).optional(),
});
export const ProductsIndexResponseBodySchema = z.object({
meta: z.record(z.string(), z.unknown()).optional(),
pagination: OffsetPaginationSchema,
products: z.array(ProductSchema),
});
export const ProductsShowPathParamsSchema = z.object({ id: z.string() });
export const ProductsShowResponseBodySchema = z.object({
meta: z.record(z.string(), z.unknown()).optional(),
product: ProductSchema,
});
export const ProductsCreateRequestBodySchema = z.object({
product: ProductCreatePayloadSchema,
});
export const ProductsCreateResponseBodySchema = z.object({
meta: z.record(z.string(), z.unknown()).optional(),
product: ProductSchema,
});
export const ProductsUpdatePathParamsSchema = z.object({ id: z.string() });
export const ProductsUpdateRequestBodySchema = z.object({
product: ProductUpdatePayloadSchema,
});
export const ProductsUpdateResponseBodySchema = z.object({
meta: z.record(z.string(), z.unknown()).optional(),
product: ProductSchema,
});
export const ProductsDestroyPathParamsSchema = z.object({ id: z.string() });
export type ProductsIndexMethod = 'GET';
export type ProductsIndexPath = '/products';
export interface ProductsIndexRequestQuery {
filter?: ProductFilter | ProductFilter[];
page?: ProductPage;
sort?: ProductSort | ProductSort[];
}
export type ProductsIndexResponseBody = {
meta?: Record<string, unknown>;
pagination: OffsetPagination;
products: Product[];
};
export interface ProductsIndexRequest {
query: ProductsIndexRequestQuery;
}
export interface ProductsIndexResponse {
body: ProductsIndexResponseBody;
}
export interface ProductsIndex {
method: ProductsIndexMethod;
path: ProductsIndexPath;
request: ProductsIndexRequest;
response: ProductsIndexResponse;
}
export type ProductsShowMethod = 'GET';
export type ProductsShowPath = '/products/:id';
export interface ProductsShowPathParams {
id: string;
}
export type ProductsShowResponseBody = {
meta?: Record<string, unknown>;
product: Product;
};
export interface ProductsShowResponse {
body: ProductsShowResponseBody;
}
export interface ProductsShow {
method: ProductsShowMethod;
path: ProductsShowPath;
pathParams: ProductsShowPathParams;
response: ProductsShowResponse;
}
export type ProductsCreateMethod = 'POST';
export type ProductsCreatePath = '/products';
export interface ProductsCreateRequestBody {
product: ProductCreatePayload;
}
export type ProductsCreateResponseBody = {
meta?: Record<string, unknown>;
product: Product;
};
export interface ProductsCreateRequest {
body: ProductsCreateRequestBody;
}
export interface ProductsCreateResponse {
body: ProductsCreateResponseBody;
}
export type ProductsCreateErrors = 422;
export interface ProductsCreate {
errors: ProductsCreateErrors;
method: ProductsCreateMethod;
path: ProductsCreatePath;
request: ProductsCreateRequest;
response: ProductsCreateResponse;
}
export type ProductsUpdateMethod = 'PATCH';
export type ProductsUpdatePath = '/products/:id';
export interface ProductsUpdatePathParams {
id: string;
}
export interface ProductsUpdateRequestBody {
product: ProductUpdatePayload;
}
export type ProductsUpdateResponseBody = {
meta?: Record<string, unknown>;
product: Product;
};
export interface ProductsUpdateRequest {
body: ProductsUpdateRequestBody;
}
export interface ProductsUpdateResponse {
body: ProductsUpdateResponseBody;
}
export type ProductsUpdateErrors = 422;
export interface ProductsUpdate {
errors: ProductsUpdateErrors;
method: ProductsUpdateMethod;
path: ProductsUpdatePath;
pathParams: ProductsUpdatePathParams;
request: ProductsUpdateRequest;
response: ProductsUpdateResponse;
}
export type ProductsDestroyMethod = 'DELETE';
export type ProductsDestroyPath = '/products/:id';
export interface ProductsDestroyPathParams {
id: string;
}
export interface ProductsDestroy {
method: ProductsDestroyMethod;
path: ProductsDestroyPath;
pathParams: ProductsDestroyPathParams;
}Sorbus
ts
import * as z from 'zod';
export const ErrorLayerSchema = z.enum(['contract', 'domain', 'http']);
export const SortDirectionSchema = z.enum(['asc', 'desc']);
export const ErrorIssueSchema = z.object({
code: z.string(),
detail: z.string(),
meta: z.record(z.string(), z.unknown()),
path: z.array(z.union([z.string(), z.number().int()])),
pointer: z.string(),
});
export const OffsetPaginationSchema = z.object({
current: z.number().int(),
items: z.number().int(),
next: z.number().int().nullable().optional(),
prev: z.number().int().nullable().optional(),
total: z.number().int(),
});
export const StringFilterSchema = z.object({
contains: z.string().optional(),
endsWith: z.string().optional(),
eq: z.string().optional(),
in: z.array(z.string()).optional(),
startsWith: z.string().optional(),
});
export const ErrorSchema = z.object({
issues: z.array(ErrorIssueSchema),
layer: ErrorLayerSchema,
});
export type ErrorLayer = 'contract' | 'domain' | 'http';
export type SortDirection = 'asc' | 'desc';
export interface ErrorIssue {
code: string;
detail: string;
meta: Record<string, unknown>;
path: string | number[];
pointer: string;
}
export interface OffsetPagination {
current: number;
items: number;
next?: number | null;
prev?: number | null;
total: number;
}
export interface StringFilter {
contains?: string;
endsWith?: string;
eq?: string;
in?: string[];
startsWith?: string;
}
export interface Error {
issues: ErrorIssue[];
layer: ErrorLayer;
}ts
import type { ProductsOperationTree } from './endpoints';
import { createClientFactory } from 'sorbus';
import { contract } from './contract';
export interface Client {
products: ProductsOperationTree;
}
export const createClient = createClientFactory<Client>(contract);ts
import { ErrorSchema } from './api';
import { products } from './endpoints';
export const contract = {
endpoints: {
products,
},
error: ErrorSchema,
} as const;ts
export * from './product';ts
import type { SortDirection, StringFilter } from '../api';
import * as z from 'zod';
import { SortDirectionSchema, StringFilterSchema } from '../api';
export const ProductFilterSchema: z.ZodType<ProductFilter> = z.lazy(() =>
z.object({
AND: z.array(ProductFilterSchema).optional(),
category: z.union([z.string(), StringFilterSchema]).optional(),
NOT: ProductFilterSchema.optional(),
name: z.union([z.string(), StringFilterSchema]).optional(),
OR: z.array(ProductFilterSchema).optional(),
}),
);
export const ProductSchema = z.object({
category: z.string(),
createdAt: z.iso.datetime(),
id: z.string(),
name: z.string(),
price: z.number(),
updatedAt: z.iso.datetime(),
});
export const ProductCreatePayloadSchema = z.object({
category: z.string(),
name: z.string(),
price: z.number().min(-99999999.99).max(99999999.99),
});
export const ProductPageSchema = z.object({
number: z.number().int().min(1).optional(),
size: z.number().int().min(1).max(100).optional(),
});
export const ProductSortSchema = z.object({
createdAt: SortDirectionSchema.optional(),
price: SortDirectionSchema.optional(),
updatedAt: SortDirectionSchema.optional(),
});
export const ProductUpdatePayloadSchema = z.object({
category: z.string().optional(),
name: z.string().optional(),
price: z.number().min(-99999999.99).max(99999999.99).optional(),
});
export interface ProductFilter {
AND?: ProductFilter[];
category?: string | StringFilter;
NOT?: ProductFilter;
name?: string | StringFilter;
OR?: ProductFilter[];
}
export interface Product {
category: string;
createdAt: string;
id: string;
name: string;
price: number;
updatedAt: string;
}
export interface ProductCreatePayload {
category: string;
name: string;
price: number;
}
export interface ProductPage {
number?: number;
size?: number;
}
export interface ProductSort {
createdAt?: SortDirection;
price?: SortDirection;
updatedAt?: SortDirection;
}
export interface ProductUpdatePayload {
category?: string;
name?: string;
price?: number;
}ts
export * from './products';ts
import type { Operation } from 'sorbus';
import type { OffsetPagination } from '../api';
import type {
Product,
ProductCreatePayload,
ProductFilter,
ProductPage,
ProductSort,
ProductUpdatePayload,
} from '../domains/product';
import * as z from 'zod';
import { OffsetPaginationSchema } from '../api';
import {
ProductCreatePayloadSchema,
ProductFilterSchema,
ProductPageSchema,
ProductSchema,
ProductSortSchema,
ProductUpdatePayloadSchema,
} from '../domains/product';
export const ProductsIndexRequestQuerySchema = z.object({
filter: z
.union([ProductFilterSchema, z.array(ProductFilterSchema)])
.optional(),
page: ProductPageSchema.optional(),
sort: z.union([ProductSortSchema, z.array(ProductSortSchema)]).optional(),
});
export const ProductsIndexResponseBodySchema = z.object({
meta: z.record(z.string(), z.unknown()).optional(),
pagination: OffsetPaginationSchema,
products: z.array(ProductSchema),
});
export const ProductsShowPathParamsSchema = z.object({ id: z.string() });
export const ProductsShowResponseBodySchema = z.object({
meta: z.record(z.string(), z.unknown()).optional(),
product: ProductSchema,
});
export const ProductsCreateRequestBodySchema = z.object({
product: ProductCreatePayloadSchema,
});
export const ProductsCreateResponseBodySchema = z.object({
meta: z.record(z.string(), z.unknown()).optional(),
product: ProductSchema,
});
export const ProductsUpdatePathParamsSchema = z.object({ id: z.string() });
export const ProductsUpdateRequestBodySchema = z.object({
product: ProductUpdatePayloadSchema,
});
export const ProductsUpdateResponseBodySchema = z.object({
meta: z.record(z.string(), z.unknown()).optional(),
product: ProductSchema,
});
export const ProductsDestroyPathParamsSchema = z.object({ id: z.string() });
export type ProductsIndexMethod = 'GET';
export type ProductsIndexPath = '/products';
export interface ProductsIndexRequestQuery {
filter?: ProductFilter | ProductFilter[];
page?: ProductPage;
sort?: ProductSort | ProductSort[];
}
export type ProductsIndexResponseBody = {
meta?: Record<string, unknown>;
pagination: OffsetPagination;
products: Product[];
};
export interface ProductsIndexRequest {
query: ProductsIndexRequestQuery;
}
export interface ProductsIndexResponse {
body: ProductsIndexResponseBody;
}
export interface ProductsIndex {
method: ProductsIndexMethod;
path: ProductsIndexPath;
request: ProductsIndexRequest;
response: ProductsIndexResponse;
}
export type ProductsShowMethod = 'GET';
export type ProductsShowPath = '/products/:id';
export interface ProductsShowPathParams {
id: string;
}
export type ProductsShowResponseBody = {
meta?: Record<string, unknown>;
product: Product;
};
export interface ProductsShowResponse {
body: ProductsShowResponseBody;
}
export interface ProductsShow {
method: ProductsShowMethod;
path: ProductsShowPath;
pathParams: ProductsShowPathParams;
response: ProductsShowResponse;
}
export type ProductsCreateMethod = 'POST';
export type ProductsCreatePath = '/products';
export interface ProductsCreateRequestBody {
product: ProductCreatePayload;
}
export type ProductsCreateResponseBody = {
meta?: Record<string, unknown>;
product: Product;
};
export interface ProductsCreateRequest {
body: ProductsCreateRequestBody;
}
export interface ProductsCreateResponse {
body: ProductsCreateResponseBody;
}
export type ProductsCreateErrors = 422;
export interface ProductsCreate {
errors: ProductsCreateErrors;
method: ProductsCreateMethod;
path: ProductsCreatePath;
request: ProductsCreateRequest;
response: ProductsCreateResponse;
}
export type ProductsUpdateMethod = 'PATCH';
export type ProductsUpdatePath = '/products/:id';
export interface ProductsUpdatePathParams {
id: string;
}
export interface ProductsUpdateRequestBody {
product: ProductUpdatePayload;
}
export type ProductsUpdateResponseBody = {
meta?: Record<string, unknown>;
product: Product;
};
export interface ProductsUpdateRequest {
body: ProductsUpdateRequestBody;
}
export interface ProductsUpdateResponse {
body: ProductsUpdateResponseBody;
}
export type ProductsUpdateErrors = 422;
export interface ProductsUpdate {
errors: ProductsUpdateErrors;
method: ProductsUpdateMethod;
path: ProductsUpdatePath;
pathParams: ProductsUpdatePathParams;
request: ProductsUpdateRequest;
response: ProductsUpdateResponse;
}
export type ProductsDestroyMethod = 'DELETE';
export type ProductsDestroyPath = '/products/:id';
export interface ProductsDestroyPathParams {
id: string;
}
export interface ProductsDestroy {
method: ProductsDestroyMethod;
path: ProductsDestroyPath;
pathParams: ProductsDestroyPathParams;
}
export const products = {
create: {
errors: [422],
method: 'POST',
path: '/products',
request: {
body: ProductsCreateRequestBodySchema,
},
response: {
body: ProductsCreateResponseBodySchema,
},
},
destroy: {
method: 'DELETE',
path: '/products/:id',
pathParams: ProductsDestroyPathParamsSchema,
},
index: {
method: 'GET',
path: '/products',
request: {
query: ProductsIndexRequestQuerySchema,
},
response: {
body: ProductsIndexResponseBodySchema,
},
},
show: {
method: 'GET',
path: '/products/:id',
pathParams: ProductsShowPathParamsSchema,
response: {
body: ProductsShowResponseBodySchema,
},
},
update: {
errors: [422],
method: 'PATCH',
path: '/products/:id',
pathParams: ProductsUpdatePathParamsSchema,
request: {
body: ProductsUpdateRequestBodySchema,
},
response: {
body: ProductsUpdateResponseBodySchema,
},
},
} as const;
export interface ProductsOperationTree {
create: Operation<ProductsCreate>;
destroy: Operation<ProductsDestroy>;
index: Operation<ProductsIndex>;
show: Operation<ProductsShow>;
update: Operation<ProductsUpdate>;
}