SlideShare a Scribd company logo
Alexander Zinchuk
SPECIFICATION-DRIVEN
DEVELOPMENT
ajaxy_ru
Alexander Zinchuk
toptal.com/resume/
alexander-zinchuk
anywaylabs.com
Executive EngineerSoftware Architect
ajaxy_ru
Alexander Zinchuk
SPECIFICATION-DRIVEN
DEVELOPMENT
ajaxy_ru
WHAT IS A
SPECIFICATION?
SPECIFICATION-DRIVEN DEVELOPMENT
ajaxy_ru
Industry standard
for REST API spec:
(FORMER SWAGGER)
SPECIFICATION-DRIVEN DEVELOPMENT
ajaxy_ru
MAINTAINING OPENAPI SPEC
{
"swagger": "2.0",
"info": {
"title": "Flightcall API 2.0",
"description": "This document describes HTTP REST JSON API",
"version": "2.0.0"
},
"host": "api.flightcall.flightvector.com",
"basePath": "/",
"schemes": [
"https"
],
"consumes": [
"application/x-www-form-urlencoded"
],
"produces": [
"application/json"
],
"securityDefinitions": {
"token": {
"name": "Authorization",
"type": "apiKey",
"in": "header"
}
},
"paths": {
"/organizations": {
"get": {
"summary": "List all organizations",
"description": "List all organizations",
"operationId": "GET--organizations",
"responses": {
"200": {
"description": "",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Organization"
}
}
}
},
"tags": [
"Public endpoints"
]
}
},
"/organizations/{id}/ems_agencies": {
"get": {
"summary": "List all EMS agencies tied to a specific organization",
"description": "List all EMS agencies tied to a specific organization",
"operationId": "GET--organizations--id--ems_agencies",
"responses": {
"200": {
"description": "",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/EmsAgency"
}
}
}
...
openapi.json
ajaxy_ru
MAINTAINING OPENAPI SPEC
{
"swagger": "2.0",
"info": {
"title": "Flightcall API 2.0",
"description": "This document describes HTTP REST JSON API",
"version": "2.0.0"
},
"host": "api.flightcall.flightvector.com",
"basePath": "/",
"schemes": [
"https"
],
"consumes": [
"application/x-www-form-urlencoded"
],
"produces": [
"application/json"
],
"securityDefinitions": {
"token": {
"name": "Authorization",
"type": "apiKey",
"in": "header"
}
},
"paths": {
"/organizations": {
"get": {
"summary": "List all organizations",
"description": "List all organizations",
"operationId": "GET--organizations",
"responses": {
"200": {
"description": "",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Organization"
}
}
}
},
"tags": [
"Public endpoints"
]
}
},
"/organizations/{id}/ems_agencies": {
"get": {
"summary": "List all EMS agencies tied to a specific organization",
"description": "List all EMS agencies tied to a specific organization",
"operationId": "GET--organizations--id--ems_agencies",
"responses": {
"200": {
"description": "",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/EmsAgency"
}
}
}
...
openapi.json
ajaxy_ru
MAINTAINING OPENAPI SPEC
{
"swagger": "2.0",
"info": {
"title": "Flightcall API 2.0",
"description": "This document describes HTTP REST JSON API",
"version": "2.0.0"
},
"host": "api.flightcall.flightvector.com",
"basePath": "/",
"schemes": [
"https"
],
"consumes": [
"application/x-www-form-urlencoded"
],
"produces": [
"application/json"
],
"securityDefinitions": {
"token": {
"name": "Authorization",
"type": "apiKey",
"in": "header"
}
},
"paths": {
"/organizations": {
"get": {
"summary": "List all organizations",
"description": "List all organizations",
"operationId": "GET--organizations",
"responses": {
"200": {
"description": "",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Organization"
}
}
}
},
"tags": [
"Public endpoints"
]
}
},
"/organizations/{id}/ems_agencies": {
"get": {
"summary": "List all EMS agencies tied to a specific organization",
"description": "List all EMS agencies tied to a specific organization",
"operationId": "GET--organizations--id--ems_agencies",
"responses": {
"200": {
"description": "",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/EmsAgency"
}
}
}
...
openapi.json
VERBOSE AND BORING
ajaxy_ru
Attempts to optimize:
· Multiple files
· JSDoc
· Online editors and services
MAINTAINING OPENAPI SPEC
ajaxy_ru
Augmenting this presentation with
examples
MAINTAINING OPENAPI SPEC
TINYSPEC
ajaxy_ru
MAINTAINING OPENAPI SPEC
Imagine, we need to
Get users…
ajaxy_ru
User {name, age?: i, isAdmin: b}
user.models.tinyspec
MAINTAINING OPENAPI SPEC
ajaxy_ru
Imagine, we need to
Get users…
User {name, age?: i, isAdmin: b}
user.models.tinyspec
GET /users
=> {users: User[]}
users.endpoints.tinyspec
MAINTAINING OPENAPI SPEC
ajaxy_ru
Imagine, we need to
Get users…
npmjs.com/package/tinyspec
MAINTAINING OPENAPI SPEC
ajaxy_ru
User {name, age?: i, isAdmin: b}
GET /users
=> {users: User[]}
user.models.tinyspec
users.endpoints.tinyspec
{
"swagger": "2.0",
"info": {
"title": "API Example",
"description": "API Example",
"version": "1.0.0"
},
"paths": {
"/users": {
"get": {
"summary": "GET /users",
"description": "GET /users",
"operationId": "GET--users",
"responses": {
"200": {
"description": "",
"schema": {
"type": "object",
"properties": {
"users": {
"type": "array",
"items": {
"$ref": "#/definitions/User"
}
}
},
"required": [
"users"
]
}
}
}
}
}
},
"tags": [],
"definitions": {
"User": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"age": {
"type": "integer"
},
"isAdmin": {
"type": "boolean"
}
},
"required": [
"name",
"isAdmin"
]
}
}
}
openapi.json
$ tinyspec -–json
MAINTAINING OPENAPI SPEC
ajaxy_ru
Imagine, we need to
Get users…
MAINTAINING OPENAPI SPEC
.org
ajaxy_ru
YOU CAN RE-USE
SPEC IN CODE!
SPECIFICATION-DRIVEN DEVELOPMENT
ajaxy_ru
1
ENDPOINT UNIT TESTS
ajaxy_ru
npmjs.com/package/supertest rubygems.org/gems/airborne
npmjs.com/package/chai-http
1 · ENDPOINT UNIT TESTS
ajaxy_ru
1 · ENDPOINT UNIT TESTS
describe('/users', () => {
it('List all users', async () => {
const { status, body: { users } } = request.get('/users');
expect(users[0].name).to.be('string');
expect(users[0].isAdmin).to.be('boolean');
expect(users[0].age).to.be.oneOf(['boolean', null]);
});
});
describe 'GET /users' do
it 'List all users' do
get '/users'
expect_json_types('users.*', {
name: :string,
isAdmin: :boolean,
age: :integer_or_null,
})
end
end
npmjs.com/package/supertest rubygems.org/gems/airborne
User {name, age?: i, isAdmin: b}
GET /users
=> {users: User[]}
user.models.tinyspec
users.endpoints.tinyspec
{
"swagger": "2.0",
"info": {
"title": "API Example",
"description": "API Example",
"version": "1.0.0"
},
"paths": {
"/users": {
"get": {
"summary": "GET /users",
"description": "GET /users",
"operationId": "GET--users",
"responses": {
"200": {
"description": "",
"schema": {
"type": "object",
"properties": {
"users": {
"type": "array",
"items": {
"$ref": "#/definitions/User"
}
}
},
"required": [
"users"
]
}
}
}
}
}
},
"tags": [],
"definitions": {
"User": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"age": {
"type": "integer"
},
"isAdmin": {
"type": "boolean"
}
},
"required": [
"name",
"isAdmin"
]
}
}
}
openapi.json
$ tinyspec -–json
1 · ENDPOINT UNIT TESTS
ajaxy_ru
Imagine, we need to
Get users…
1 · ENDPOINT UNIT TESTS
json-schema.org
"User": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"age": {
"type": "integer"
},
"isAdmin": {
"type": "boolean"
}
},
"required": [
"name",
"isAdmin"
]
}
JSON SCHEMA
ajaxy_ru
1 · ENDPOINT UNIT TESTS
Any key-value object may be validated against JSON Schema
json-schema.org
"User": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"age": {
"type": "integer"
},
"isAdmin": {
"type": "boolean"
}
},
"required": [
"name",
"isAdmin"
]
}
JSON SCHEMA
ajaxy_ru
npmjs.com/package/jest-ajv rubygems.org/gems/json_matchers
npmjs.com/package/chai-ajv-json-schema
1 · ENDPOINT UNIT TESTS
Any key-value object may be validated against JSON Schema
ajaxy_ru
1 · ENDPOINT UNIT TESTS
import deref from 'json-schema-deref-sync';
const schemas = deref(require('./openapi.json')).definitions;
describe('/users', () => {
it('List all users', async () => {
const { status, body: { users } } = request.get('/users');
expect(users[0]).toMatchSchema(schemas.User);
});
});
require ‘json_matchers/rspec'
JsonMatchers.schema_root = 'spec/schemas'
describe 'GET /users' do
it 'List all users' do
get '/users'
expect(result[:users][0]).to match_json_schema('User')
end
end
npmjs.com/package/jest-ajv rubygems.org/gems/json_matchers
2
USER-DATA VALIDATION
ajaxy_ru
2 · USER-DATA VALIDATION
User {name, age?: i, isAdmin: b}
user.models.tinyspec
Imagine, we need to
Update a user…
ajaxy_ru
2 · USER-DATA VALIDATION
User {name, age?: i, isAdmin: b}
UserUpdate !{name?, age?: i}
user.models.tinyspec
Imagine, we need to
Update a user…
ajaxy_ru
2 · USER-DATA VALIDATION
User {name, age?: i, isAdmin: b}
UserUpdate !{name?, age?: i}
user.models.tinyspec
Imagine, we need to
Update a user…
ajaxy_ru
2 · USER-DATA VALIDATION
User {name, age?: i, isAdmin: b}
UserUpdate !{name?, age?: i}
user.models.tinyspec
Imagine, we need to
Update a user…
ajaxy_ru
PATCH /users/:id {user: UserUpdate}
=> {success: b}
users.endpoints.tinyspec
2 · USER-DATA VALIDATION
UserUpdate !{name?, age?: i}
user.models.tinyspec
Imagine, we need to
Update a user…
ajaxy_ru
UserUpdate !{name?, age?: i}
PATCH /users/:id {user: UserUpdate}
=> {success: b}
user.models.tinyspec
users.endpoints.tinyspec
2 · USER-DATA VALIDATION
...
"paths": {
"/users/{id}": {
"patch": {
"summary": "PATCH /users/:id",
"description": "PATCH /users/:id",
"operationId": "PATCH--users--id",
"responses": {
"200": {
"description": "",
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
},
"required": [
"success"
]
}
}
},
"parameters": [
{
"name": "id",
"type": "string",
"in": "path",
"required": true
},
{
"name": "body",
"required": true,
"schema": {
"type": "object",
"properties": {
"user": {
"$ref": "#/definitions/UserUpdate"
}
},
"required": [
"user"
]
},
"in": "body"
}
]
}
}
},
"tags": [],
"definitions": {
"UserUpdate": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"age": {
"type": "integer"
}
},
"additionalProperties": false
}
}
openapi.json
Imagine, we need to
Update a user…
$ tinyspec -–json
ajaxy_ru
npmjs.com/package/ajv rubygems.org/gems/json-schema
2 · USER-DATA VALIDATION
ajaxy_ru
router.patch('/:id', async (ctx) => {
const updateData = ctx.body.user;
// Validation using JSON schema from API specification.
await validate(schemas.UserUpdate, updateData);
const user = await User.findById(ctx.params.id);
await user.update(updateData);
ctx.body = { success: true };
});
2 · USER-DATA VALIDATION
ajaxy_ru
router.patch('/:id', async (ctx) => {
const updateData = ctx.body.user;
// Validation using JSON schema from API specification.
await validate(schemas.UserUpdate, updateData);
const user = await User.findById(ctx.params.id);
await user.update(updateData);
ctx.body = { success: true };
});
2 · USER-DATA VALIDATION
FieldsValidationError {
error: b,
message,
fields: {name, message}[]
}
error.models.tinyspec
ajaxy_ru
router.patch('/:id', async (ctx) => {
const updateData = ctx.body.user;
// Validation using JSON schema from API specification.
await validate(schemas.UserUpdate, updateData);
const user = await User.findById(ctx.params.id);
await user.update(updateData);
ctx.body = { success: true };
});
2 · USER-DATA VALIDATION
FieldsValidationError {
error: b,
message,
fields: {name, message}[]
}
error.models.tinyspec
PATCH /users/:id {user: UserUpdate}
=> 200 {success: b}
=> 422 FieldsValidationError
users.endpoints.tinyspec
ajaxy_ru
3
MODEL SERIALIZATION
ajaxy_ru
3 · MODEL SERIALIZATION
{…}
DB TABLE JSON VIEWORM MODEL
ajaxy_ru
{…}
ORM MODEL JSON VIEW
serialization
DB TABLE
3 · MODEL SERIALIZATION
ajaxy_ru
{…}
{…}
{…}
DB TABLE
ASSOCIATED
MODELS ALTERNATE
REQUESTS
3 · MODEL SERIALIZATION
ajaxy_ru
Our Spec
has all needed
information!
3 · MODEL SERIALIZATION
{…}
{…}
{…}
ALTERNATE
REQUESTSajaxy_ru
Imagine, we need to
Get all users
with posts
and comments…
3 · MODEL SERIALIZATION
ajaxy_ru
Comment {authorId: i, message}
Post {topic, message, comments?: Comment[]}
User {name, isAdmin: b, age?: i}
UserWithPosts < User {posts: Post[]}
models.tinyspec
3 · MODEL SERIALIZATION
ajaxy_ru
Comment {authorId: i, message}
Post {topic, message, comments?: Comment[]}
User {name, isAdmin: b, age?: i}
UserWithPosts < User {posts: Post[]}
GET /blog/users
=> {users: UserWithPosts[]}
models.tinyspec
blogUsers.endpoints.tinyspec
3 · MODEL SERIALIZATION
ajaxy_ru
Comment {authorId: i, message}
Post {topic, message, comments?: Comment[]}
User {name, isAdmin: b, age?: i}
UserWithPosts < User {posts: Post[]}
GET /blog/users
=> {users: UserWithPosts[]}
models.tinyspec
blogUsers.endpoints.tinyspec
3 · MODEL SERIALIZATION
…
»paths»: {
"/blog/users": {
"get": {
"summary": "GET /blog/users",
"description": "GET /blog/users",
"operationId": "GET--blog--users",
"responses": {
"200": {
"description": "",
"schema": {
"type": "object",
"properties": {
"users": {
"type": "array",
"items": {
"$ref": "#/definitions/UserWithPosts"
}
}
},
"required": [
"users"
]
}
}
}
}
}
},
"tags": [],
"definitions": {
"Comment": {
"type": "object",
"properties": {
"authorId": {
"type": "integer"
},
"message": {
"type": "string"
}
},
"required": [
"authorId",
"message"
]
},
"Post": {
"type": "object",
"properties": {
"topic": {
"type": "string"
},
"message": {
"type": "string"
},
"comments": {
"type": "array",
"items": {
"$ref": "#/definitions/Comment"
}
}
},
"required": [
"topic",
"message"
]
},
"User": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"isAdmin": {
"type": "boolean"
},
"age": {
"type": "integer"
}
},
"required": [
"name",
"isAdmin"
]
},
"UserWithPosts": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"isAdmin": {
"type": "boolean"
},
"age": {
"type": "integer"
},
"posts": {
"type": "array",
"items": {
"$ref": "#/definitions/Post"
}
}
},
"required": [
"name",
"isAdmin",
"posts"
]
}
}
openapi.json
$ tinyspec -–json
ajaxy_ru
3 · MODEL SERIALIZATION
npmjs.com/package/sequelize-serialize
ajaxy_ru
3 · MODEL SERIALIZATION
import serialize from 'sequelize-serialize';
router.get('/blog/users', async (ctx) => {
const users = await User.findAll({
include: [{
association: User.posts,
include: [
Post.comments
]
}]
});
ctx.body = serialize(users, schemas.UserWithPosts);
});
npmjs.com/package/sequelize-serialize
ajaxy_ru
-=MAGIC=-
4
STATIC TYPING
ajaxy_ru
4 · STATIC TYPING
JSON SCHEMA
ajaxy_ru
npmjs.com/package/sw2dts
npmjs.com/package/swagger-to-flowtype
4 · STATIC TYPING
ajaxy_ru
4 · STATIC TYPING
$ tinyspec -–json
$ sw2dts ./openapi.json -o Api.d.ts --namespace Api
ajaxy_ru
4 · STATIC TYPING
$ tinyspec -–json
$ sw2dts ./openapi.json -o Api.d.ts --namespace Api
declare namespace Api {
export interface Comment {
authorId: number;
message: string;
}
export interface Post {
topic: string;
message: string;
comments?: Comment[];
}
export interface User {
name: string;
isAdmin: boolean;
age?: number;
}
export interface UserUpdate {
name?: string;
age?: number;
}
export interface UserWithPosts {
name: string;
isAdmin: boolean;
age?: number;
posts: Post[];
}
}
Api.d.ts
router.patch('/users/:id', async (ctx) => {
// Specify type for request data object
const userData: Api.UserUpdate = ctx.request.body.user;
// Run spec validation
await validate(schemas.UserUpdate, userData);
// Query the database
const user = await User.findById(ctx.params.id);
await user.update(userData);
// Return serialized result
const serialized: Api.User = serialize(user, schemas.User);
ctx.body = { user: serialized };
});
4 · STATIC TYPING
ajaxy_ru
it('Update user', async () => {
// Static check for test input data.
const updateData: Api.UserUpdate = { name: MODIFIED };
const res = await request.patch('/users/1', { user: updateData });
// Type helper for request response:
const user: Api.User = res.body.user;
expect(user).to.be.validWithSchema(schemas.User);
expect(user).to.containSubset(updateData);
});
4 · STATIC TYPING
router.patch('/users/:id', async (ctx) => {
// Specify type for request data object
const userData: Api.UserUpdate = ctx.request.body.user;
// Run spec validation
await validate(schemas.UserUpdate, userData);
// Query the database
const user = await User.findById(ctx.params.id);
await user.update(userData);
// Return serialized result
const serialized: Api.User = serialize(user, schemas.User);
ctx.body = { user: serialized };
});
ajaxy_ru
5
TYPE CASTING
ajaxy_ru
5 · TYPE CASTING
param1=value&param2=777&param3=false
Query params or non-JSON body:
ajaxy_ru
5 · TYPE CASTING
param1=value&param2=777&param3=false
{
param1: 'value',
param2: '777',
param3: 'false'
}
Query params or non-JSON body:
ajaxy_ru
npmjs.com/package/cast-with-schema
5 · TYPE CASTING
ajaxy_ru
import castWithSchema from 'cast-with-schema';
router.get('/posts', async (ctx) => {
// Cast parameters to expected types
const query = castWithSchema(ctx.query, schemas.PostsQuery);
// Run spec validation
await validate(schemas.PostsQuery, query);
// Query the database
const posts = await Post.search(query);
// Return serialized result
ctx.body = { posts: serialize(posts, schemas.Post) };
});
npmjs.com/package/cast-with-schema
5 · TYPE CASTING
ajaxy_ru
THANK YOU!
ajaxy_ru
github.com/Ajaxy/tinyspec anywaylabs.com

More Related Content

PDF
Specification-Driven Development of REST APIs by Alexander Zinchuk
PPTX
Stratalux Cloud Formation and Chef Integration Presentation
PDF
HTML5: friend or foe (to Flash)?
PPTX
JavaFX 2.0 With Alternative Languages [Portuguese]
PPT
JavaScript Misunderstood
PDF
Intro to HTML5
PDF
Ams adapters
PDF
Um roadmap do Framework Ruby on Rails, do Rails 1 ao Rails 4 - DevDay 2013
Specification-Driven Development of REST APIs by Alexander Zinchuk
Stratalux Cloud Formation and Chef Integration Presentation
HTML5: friend or foe (to Flash)?
JavaFX 2.0 With Alternative Languages [Portuguese]
JavaScript Misunderstood
Intro to HTML5
Ams adapters
Um roadmap do Framework Ruby on Rails, do Rails 1 ao Rails 4 - DevDay 2013

What's hot (19)

PDF
codebits 2009 HTML5 JS APIs
TXT
Agile Testing Days 2018 - API Fundamentals - postman collection
PDF
Advanced JavaScript Development
PDF
Jacob Waller: Webifying Titanium Development
PDF
Angular를 활용한 웹 프론트단 개발과 2.0에서 달라진점
PDF
jQuery & 10,000 Global Functions: Working with Legacy JavaScript
PDF
Micro app-framework
PDF
Web2.0 with jQuery in English
KEY
An in-depth look at jQuery UI
KEY
Html5 For Jjugccc2009fall
PDF
Ajax Under The Hood
PDF
Alloy Tips & Tricks #TiLon
PDF
WCLA12 JavaScript
PPTX
Angular Tutorial Freshers and Experienced
PDF
RESTFUL SERVICES MADE EASY: THE EVE REST API FRAMEWORK - Nicola Iarocci - Co...
PDF
TestWorks conf Dry up your angularjs unit tests using mox - Mike Woudenberg
PDF
Cache Money Talk: Practical Application
PDF
Enter the app era with ruby on rails
PDF
jQuery (DrupalCamp Toronto)
codebits 2009 HTML5 JS APIs
Agile Testing Days 2018 - API Fundamentals - postman collection
Advanced JavaScript Development
Jacob Waller: Webifying Titanium Development
Angular를 활용한 웹 프론트단 개발과 2.0에서 달라진점
jQuery & 10,000 Global Functions: Working with Legacy JavaScript
Micro app-framework
Web2.0 with jQuery in English
An in-depth look at jQuery UI
Html5 For Jjugccc2009fall
Ajax Under The Hood
Alloy Tips & Tricks #TiLon
WCLA12 JavaScript
Angular Tutorial Freshers and Experienced
RESTFUL SERVICES MADE EASY: THE EVE REST API FRAMEWORK - Nicola Iarocci - Co...
TestWorks conf Dry up your angularjs unit tests using mox - Mike Woudenberg
Cache Money Talk: Practical Application
Enter the app era with ruby on rails
jQuery (DrupalCamp Toronto)
Ad

Similar to APIdays Zurich 2019 - Specification Driven Development for REST APIS Alexander Zinchuck, Toptal (20)

PDF
JSON API Specificiation
PPT
Advanced Json
PDF
Testing swagger contracts without contract based testing
PDF
JSON API Standards
PPTX
REST with Eve and Python
PDF
Eve - REST API for Humans™
PDF
Dead Simple APIs with OpenAPI
PDF
Easy REST with OpenAPI
PDF
Easy REST with OpenAPI
PPTX
Automatic discovery of Web API Specifications: an example-driven approach
PPTX
Example-driven Web API Specification Discovery
PPTX
Introducing OpenAPI Version 3.1
PPTX
SVQdotNET: Building APIs with OpenApi
PDF
КОСТЯНТИН КЛЮЄВ «Postman: API Automation Testing Swiss Army Knife» Kyiv QADay...
PDF
Json at work overview and ecosystem-v2.0
DOCX
Api testing bible using postman
PDF
Enforcing API Design Rules for High Quality Code Generation
PDF
Building Awesome APIs in Grails
PDF
JSON Schema in Web Frontend #insideFE
PDF
How to make the most out of servant (ZuriHac 2021)
JSON API Specificiation
Advanced Json
Testing swagger contracts without contract based testing
JSON API Standards
REST with Eve and Python
Eve - REST API for Humans™
Dead Simple APIs with OpenAPI
Easy REST with OpenAPI
Easy REST with OpenAPI
Automatic discovery of Web API Specifications: an example-driven approach
Example-driven Web API Specification Discovery
Introducing OpenAPI Version 3.1
SVQdotNET: Building APIs with OpenApi
КОСТЯНТИН КЛЮЄВ «Postman: API Automation Testing Swiss Army Knife» Kyiv QADay...
Json at work overview and ecosystem-v2.0
Api testing bible using postman
Enforcing API Design Rules for High Quality Code Generation
Building Awesome APIs in Grails
JSON Schema in Web Frontend #insideFE
How to make the most out of servant (ZuriHac 2021)
Ad

More from apidays (20)

PDF
apidays Munich 2025 - The Physics of Requirement Sciences Through Application...
PDF
apidays Munich 2025 - Developer Portals, API Catalogs, and Marketplaces, Miri...
PDF
apidays Munich 2025 - Making Sense of AI-Ready APIs in a Buzzword World, Andr...
PDF
apidays Munich 2025 - Integrate Your APIs into the New AI Marketplace, Senthi...
PDF
apidays Munich 2025 - The Double Life of the API Product Manager, Emmanuel Pa...
PDF
apidays Munich 2025 - Let’s build, debug and test a magic MCP server in Postm...
PDF
apidays Munich 2025 - The life-changing magic of great API docs, Jens Fischer...
PDF
apidays Munich 2025 - Automating Operations Without Reinventing the Wheel, Ma...
PDF
apidays Munich 2025 - Geospatial Artificial Intelligence (GeoAI) with OGC API...
PPTX
apidays Munich 2025 - GraphQL 101: I won't REST, until you GraphQL, Surbhi Si...
PPTX
apidays Munich 2025 - Effectively incorporating API Security into the overall...
PPTX
apidays Munich 2025 - Federated API Management and Governance, Vince Baker (D...
PPTX
apidays Munich 2025 - Agentic AI: A Friend or Foe?, Merja Kajava (Aavista Oy)
PPTX
apidays Munich 2025 - Streamline & Secure LLM Traffic with APISIX AI Gateway ...
PPTX
apidays Munich 2025 - Building Telco-Aware Apps with Open Gateway APIs, Subhr...
PPTX
apidays Munich 2025 - Building an AWS Serverless Application with Terraform, ...
PDF
apidays Helsinki & North 2025 - REST in Peace? Hunting the Dominant Design fo...
PDF
apidays Helsinki & North 2025 - Monetizing AI APIs: The New API Economy, Alla...
PDF
apidays Helsinki & North 2025 - How (not) to run a Graphql Stewardship Group,...
PDF
apidays Helsinki & North 2025 - APIs in the healthcare sector: hospitals inte...
apidays Munich 2025 - The Physics of Requirement Sciences Through Application...
apidays Munich 2025 - Developer Portals, API Catalogs, and Marketplaces, Miri...
apidays Munich 2025 - Making Sense of AI-Ready APIs in a Buzzword World, Andr...
apidays Munich 2025 - Integrate Your APIs into the New AI Marketplace, Senthi...
apidays Munich 2025 - The Double Life of the API Product Manager, Emmanuel Pa...
apidays Munich 2025 - Let’s build, debug and test a magic MCP server in Postm...
apidays Munich 2025 - The life-changing magic of great API docs, Jens Fischer...
apidays Munich 2025 - Automating Operations Without Reinventing the Wheel, Ma...
apidays Munich 2025 - Geospatial Artificial Intelligence (GeoAI) with OGC API...
apidays Munich 2025 - GraphQL 101: I won't REST, until you GraphQL, Surbhi Si...
apidays Munich 2025 - Effectively incorporating API Security into the overall...
apidays Munich 2025 - Federated API Management and Governance, Vince Baker (D...
apidays Munich 2025 - Agentic AI: A Friend or Foe?, Merja Kajava (Aavista Oy)
apidays Munich 2025 - Streamline & Secure LLM Traffic with APISIX AI Gateway ...
apidays Munich 2025 - Building Telco-Aware Apps with Open Gateway APIs, Subhr...
apidays Munich 2025 - Building an AWS Serverless Application with Terraform, ...
apidays Helsinki & North 2025 - REST in Peace? Hunting the Dominant Design fo...
apidays Helsinki & North 2025 - Monetizing AI APIs: The New API Economy, Alla...
apidays Helsinki & North 2025 - How (not) to run a Graphql Stewardship Group,...
apidays Helsinki & North 2025 - APIs in the healthcare sector: hospitals inte...

Recently uploaded (20)

PDF
Assigned Numbers - 2025 - Bluetooth® Document
PDF
Microsoft Solutions Partner Drive Digital Transformation with D365.pdf
PDF
7 ChatGPT Prompts to Help You Define Your Ideal Customer Profile.pdf
PDF
A novel scalable deep ensemble learning framework for big data classification...
PPTX
OMC Textile Division Presentation 2021.pptx
PDF
Approach and Philosophy of On baking technology
PDF
Univ-Connecticut-ChatGPT-Presentaion.pdf
PDF
A comparative analysis of optical character recognition models for extracting...
PDF
Transform Your ITIL® 4 & ITSM Strategy with AI in 2025.pdf
PPTX
Chapter 5: Probability Theory and Statistics
PDF
Zenith AI: Advanced Artificial Intelligence
PDF
August Patch Tuesday
PDF
Encapsulation_ Review paper, used for researhc scholars
PDF
MIND Revenue Release Quarter 2 2025 Press Release
PDF
Agricultural_Statistics_at_a_Glance_2022_0.pdf
PDF
1 - Historical Antecedents, Social Consideration.pdf
PPTX
Digital-Transformation-Roadmap-for-Companies.pptx
PDF
Hindi spoken digit analysis for native and non-native speakers
PDF
Building Integrated photovoltaic BIPV_UPV.pdf
PPTX
A Presentation on Artificial Intelligence
Assigned Numbers - 2025 - Bluetooth® Document
Microsoft Solutions Partner Drive Digital Transformation with D365.pdf
7 ChatGPT Prompts to Help You Define Your Ideal Customer Profile.pdf
A novel scalable deep ensemble learning framework for big data classification...
OMC Textile Division Presentation 2021.pptx
Approach and Philosophy of On baking technology
Univ-Connecticut-ChatGPT-Presentaion.pdf
A comparative analysis of optical character recognition models for extracting...
Transform Your ITIL® 4 & ITSM Strategy with AI in 2025.pdf
Chapter 5: Probability Theory and Statistics
Zenith AI: Advanced Artificial Intelligence
August Patch Tuesday
Encapsulation_ Review paper, used for researhc scholars
MIND Revenue Release Quarter 2 2025 Press Release
Agricultural_Statistics_at_a_Glance_2022_0.pdf
1 - Historical Antecedents, Social Consideration.pdf
Digital-Transformation-Roadmap-for-Companies.pptx
Hindi spoken digit analysis for native and non-native speakers
Building Integrated photovoltaic BIPV_UPV.pdf
A Presentation on Artificial Intelligence

APIdays Zurich 2019 - Specification Driven Development for REST APIS Alexander Zinchuck, Toptal

  • 5. Industry standard for REST API spec: (FORMER SWAGGER) SPECIFICATION-DRIVEN DEVELOPMENT ajaxy_ru
  • 6. MAINTAINING OPENAPI SPEC { "swagger": "2.0", "info": { "title": "Flightcall API 2.0", "description": "This document describes HTTP REST JSON API", "version": "2.0.0" }, "host": "api.flightcall.flightvector.com", "basePath": "/", "schemes": [ "https" ], "consumes": [ "application/x-www-form-urlencoded" ], "produces": [ "application/json" ], "securityDefinitions": { "token": { "name": "Authorization", "type": "apiKey", "in": "header" } }, "paths": { "/organizations": { "get": { "summary": "List all organizations", "description": "List all organizations", "operationId": "GET--organizations", "responses": { "200": { "description": "", "schema": { "type": "array", "items": { "$ref": "#/definitions/Organization" } } } }, "tags": [ "Public endpoints" ] } }, "/organizations/{id}/ems_agencies": { "get": { "summary": "List all EMS agencies tied to a specific organization", "description": "List all EMS agencies tied to a specific organization", "operationId": "GET--organizations--id--ems_agencies", "responses": { "200": { "description": "", "schema": { "type": "array", "items": { "$ref": "#/definitions/EmsAgency" } } } ... openapi.json ajaxy_ru
  • 7. MAINTAINING OPENAPI SPEC { "swagger": "2.0", "info": { "title": "Flightcall API 2.0", "description": "This document describes HTTP REST JSON API", "version": "2.0.0" }, "host": "api.flightcall.flightvector.com", "basePath": "/", "schemes": [ "https" ], "consumes": [ "application/x-www-form-urlencoded" ], "produces": [ "application/json" ], "securityDefinitions": { "token": { "name": "Authorization", "type": "apiKey", "in": "header" } }, "paths": { "/organizations": { "get": { "summary": "List all organizations", "description": "List all organizations", "operationId": "GET--organizations", "responses": { "200": { "description": "", "schema": { "type": "array", "items": { "$ref": "#/definitions/Organization" } } } }, "tags": [ "Public endpoints" ] } }, "/organizations/{id}/ems_agencies": { "get": { "summary": "List all EMS agencies tied to a specific organization", "description": "List all EMS agencies tied to a specific organization", "operationId": "GET--organizations--id--ems_agencies", "responses": { "200": { "description": "", "schema": { "type": "array", "items": { "$ref": "#/definitions/EmsAgency" } } } ... openapi.json ajaxy_ru
  • 8. MAINTAINING OPENAPI SPEC { "swagger": "2.0", "info": { "title": "Flightcall API 2.0", "description": "This document describes HTTP REST JSON API", "version": "2.0.0" }, "host": "api.flightcall.flightvector.com", "basePath": "/", "schemes": [ "https" ], "consumes": [ "application/x-www-form-urlencoded" ], "produces": [ "application/json" ], "securityDefinitions": { "token": { "name": "Authorization", "type": "apiKey", "in": "header" } }, "paths": { "/organizations": { "get": { "summary": "List all organizations", "description": "List all organizations", "operationId": "GET--organizations", "responses": { "200": { "description": "", "schema": { "type": "array", "items": { "$ref": "#/definitions/Organization" } } } }, "tags": [ "Public endpoints" ] } }, "/organizations/{id}/ems_agencies": { "get": { "summary": "List all EMS agencies tied to a specific organization", "description": "List all EMS agencies tied to a specific organization", "operationId": "GET--organizations--id--ems_agencies", "responses": { "200": { "description": "", "schema": { "type": "array", "items": { "$ref": "#/definitions/EmsAgency" } } } ... openapi.json VERBOSE AND BORING ajaxy_ru
  • 9. Attempts to optimize: · Multiple files · JSDoc · Online editors and services MAINTAINING OPENAPI SPEC ajaxy_ru
  • 10. Augmenting this presentation with examples MAINTAINING OPENAPI SPEC TINYSPEC ajaxy_ru
  • 11. MAINTAINING OPENAPI SPEC Imagine, we need to Get users… ajaxy_ru
  • 12. User {name, age?: i, isAdmin: b} user.models.tinyspec MAINTAINING OPENAPI SPEC ajaxy_ru Imagine, we need to Get users…
  • 13. User {name, age?: i, isAdmin: b} user.models.tinyspec GET /users => {users: User[]} users.endpoints.tinyspec MAINTAINING OPENAPI SPEC ajaxy_ru Imagine, we need to Get users…
  • 15. User {name, age?: i, isAdmin: b} GET /users => {users: User[]} user.models.tinyspec users.endpoints.tinyspec { "swagger": "2.0", "info": { "title": "API Example", "description": "API Example", "version": "1.0.0" }, "paths": { "/users": { "get": { "summary": "GET /users", "description": "GET /users", "operationId": "GET--users", "responses": { "200": { "description": "", "schema": { "type": "object", "properties": { "users": { "type": "array", "items": { "$ref": "#/definitions/User" } } }, "required": [ "users" ] } } } } } }, "tags": [], "definitions": { "User": { "type": "object", "properties": { "name": { "type": "string" }, "age": { "type": "integer" }, "isAdmin": { "type": "boolean" } }, "required": [ "name", "isAdmin" ] } } } openapi.json $ tinyspec -–json MAINTAINING OPENAPI SPEC ajaxy_ru Imagine, we need to Get users…
  • 17. YOU CAN RE-USE SPEC IN CODE! SPECIFICATION-DRIVEN DEVELOPMENT ajaxy_ru
  • 20. 1 · ENDPOINT UNIT TESTS describe('/users', () => { it('List all users', async () => { const { status, body: { users } } = request.get('/users'); expect(users[0].name).to.be('string'); expect(users[0].isAdmin).to.be('boolean'); expect(users[0].age).to.be.oneOf(['boolean', null]); }); }); describe 'GET /users' do it 'List all users' do get '/users' expect_json_types('users.*', { name: :string, isAdmin: :boolean, age: :integer_or_null, }) end end npmjs.com/package/supertest rubygems.org/gems/airborne
  • 21. User {name, age?: i, isAdmin: b} GET /users => {users: User[]} user.models.tinyspec users.endpoints.tinyspec { "swagger": "2.0", "info": { "title": "API Example", "description": "API Example", "version": "1.0.0" }, "paths": { "/users": { "get": { "summary": "GET /users", "description": "GET /users", "operationId": "GET--users", "responses": { "200": { "description": "", "schema": { "type": "object", "properties": { "users": { "type": "array", "items": { "$ref": "#/definitions/User" } } }, "required": [ "users" ] } } } } } }, "tags": [], "definitions": { "User": { "type": "object", "properties": { "name": { "type": "string" }, "age": { "type": "integer" }, "isAdmin": { "type": "boolean" } }, "required": [ "name", "isAdmin" ] } } } openapi.json $ tinyspec -–json 1 · ENDPOINT UNIT TESTS ajaxy_ru Imagine, we need to Get users…
  • 22. 1 · ENDPOINT UNIT TESTS json-schema.org "User": { "type": "object", "properties": { "name": { "type": "string" }, "age": { "type": "integer" }, "isAdmin": { "type": "boolean" } }, "required": [ "name", "isAdmin" ] } JSON SCHEMA ajaxy_ru
  • 23. 1 · ENDPOINT UNIT TESTS Any key-value object may be validated against JSON Schema json-schema.org "User": { "type": "object", "properties": { "name": { "type": "string" }, "age": { "type": "integer" }, "isAdmin": { "type": "boolean" } }, "required": [ "name", "isAdmin" ] } JSON SCHEMA ajaxy_ru
  • 24. npmjs.com/package/jest-ajv rubygems.org/gems/json_matchers npmjs.com/package/chai-ajv-json-schema 1 · ENDPOINT UNIT TESTS Any key-value object may be validated against JSON Schema ajaxy_ru
  • 25. 1 · ENDPOINT UNIT TESTS import deref from 'json-schema-deref-sync'; const schemas = deref(require('./openapi.json')).definitions; describe('/users', () => { it('List all users', async () => { const { status, body: { users } } = request.get('/users'); expect(users[0]).toMatchSchema(schemas.User); }); }); require ‘json_matchers/rspec' JsonMatchers.schema_root = 'spec/schemas' describe 'GET /users' do it 'List all users' do get '/users' expect(result[:users][0]).to match_json_schema('User') end end npmjs.com/package/jest-ajv rubygems.org/gems/json_matchers
  • 27. 2 · USER-DATA VALIDATION User {name, age?: i, isAdmin: b} user.models.tinyspec Imagine, we need to Update a user… ajaxy_ru
  • 28. 2 · USER-DATA VALIDATION User {name, age?: i, isAdmin: b} UserUpdate !{name?, age?: i} user.models.tinyspec Imagine, we need to Update a user… ajaxy_ru
  • 29. 2 · USER-DATA VALIDATION User {name, age?: i, isAdmin: b} UserUpdate !{name?, age?: i} user.models.tinyspec Imagine, we need to Update a user… ajaxy_ru
  • 30. 2 · USER-DATA VALIDATION User {name, age?: i, isAdmin: b} UserUpdate !{name?, age?: i} user.models.tinyspec Imagine, we need to Update a user… ajaxy_ru
  • 31. PATCH /users/:id {user: UserUpdate} => {success: b} users.endpoints.tinyspec 2 · USER-DATA VALIDATION UserUpdate !{name?, age?: i} user.models.tinyspec Imagine, we need to Update a user… ajaxy_ru
  • 32. UserUpdate !{name?, age?: i} PATCH /users/:id {user: UserUpdate} => {success: b} user.models.tinyspec users.endpoints.tinyspec 2 · USER-DATA VALIDATION ... "paths": { "/users/{id}": { "patch": { "summary": "PATCH /users/:id", "description": "PATCH /users/:id", "operationId": "PATCH--users--id", "responses": { "200": { "description": "", "schema": { "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ] } } }, "parameters": [ { "name": "id", "type": "string", "in": "path", "required": true }, { "name": "body", "required": true, "schema": { "type": "object", "properties": { "user": { "$ref": "#/definitions/UserUpdate" } }, "required": [ "user" ] }, "in": "body" } ] } } }, "tags": [], "definitions": { "UserUpdate": { "type": "object", "properties": { "name": { "type": "string" }, "age": { "type": "integer" } }, "additionalProperties": false } } openapi.json Imagine, we need to Update a user… $ tinyspec -–json ajaxy_ru
  • 34. router.patch('/:id', async (ctx) => { const updateData = ctx.body.user; // Validation using JSON schema from API specification. await validate(schemas.UserUpdate, updateData); const user = await User.findById(ctx.params.id); await user.update(updateData); ctx.body = { success: true }; }); 2 · USER-DATA VALIDATION ajaxy_ru
  • 35. router.patch('/:id', async (ctx) => { const updateData = ctx.body.user; // Validation using JSON schema from API specification. await validate(schemas.UserUpdate, updateData); const user = await User.findById(ctx.params.id); await user.update(updateData); ctx.body = { success: true }; }); 2 · USER-DATA VALIDATION FieldsValidationError { error: b, message, fields: {name, message}[] } error.models.tinyspec ajaxy_ru
  • 36. router.patch('/:id', async (ctx) => { const updateData = ctx.body.user; // Validation using JSON schema from API specification. await validate(schemas.UserUpdate, updateData); const user = await User.findById(ctx.params.id); await user.update(updateData); ctx.body = { success: true }; }); 2 · USER-DATA VALIDATION FieldsValidationError { error: b, message, fields: {name, message}[] } error.models.tinyspec PATCH /users/:id {user: UserUpdate} => 200 {success: b} => 422 FieldsValidationError users.endpoints.tinyspec ajaxy_ru
  • 38. 3 · MODEL SERIALIZATION {…} DB TABLE JSON VIEWORM MODEL ajaxy_ru
  • 39. {…} ORM MODEL JSON VIEW serialization DB TABLE 3 · MODEL SERIALIZATION ajaxy_ru
  • 41. Our Spec has all needed information! 3 · MODEL SERIALIZATION {…} {…} {…} ALTERNATE REQUESTSajaxy_ru
  • 42. Imagine, we need to Get all users with posts and comments… 3 · MODEL SERIALIZATION ajaxy_ru
  • 43. Comment {authorId: i, message} Post {topic, message, comments?: Comment[]} User {name, isAdmin: b, age?: i} UserWithPosts < User {posts: Post[]} models.tinyspec 3 · MODEL SERIALIZATION ajaxy_ru
  • 44. Comment {authorId: i, message} Post {topic, message, comments?: Comment[]} User {name, isAdmin: b, age?: i} UserWithPosts < User {posts: Post[]} GET /blog/users => {users: UserWithPosts[]} models.tinyspec blogUsers.endpoints.tinyspec 3 · MODEL SERIALIZATION ajaxy_ru
  • 45. Comment {authorId: i, message} Post {topic, message, comments?: Comment[]} User {name, isAdmin: b, age?: i} UserWithPosts < User {posts: Post[]} GET /blog/users => {users: UserWithPosts[]} models.tinyspec blogUsers.endpoints.tinyspec 3 · MODEL SERIALIZATION … »paths»: { "/blog/users": { "get": { "summary": "GET /blog/users", "description": "GET /blog/users", "operationId": "GET--blog--users", "responses": { "200": { "description": "", "schema": { "type": "object", "properties": { "users": { "type": "array", "items": { "$ref": "#/definitions/UserWithPosts" } } }, "required": [ "users" ] } } } } } }, "tags": [], "definitions": { "Comment": { "type": "object", "properties": { "authorId": { "type": "integer" }, "message": { "type": "string" } }, "required": [ "authorId", "message" ] }, "Post": { "type": "object", "properties": { "topic": { "type": "string" }, "message": { "type": "string" }, "comments": { "type": "array", "items": { "$ref": "#/definitions/Comment" } } }, "required": [ "topic", "message" ] }, "User": { "type": "object", "properties": { "name": { "type": "string" }, "isAdmin": { "type": "boolean" }, "age": { "type": "integer" } }, "required": [ "name", "isAdmin" ] }, "UserWithPosts": { "type": "object", "properties": { "name": { "type": "string" }, "isAdmin": { "type": "boolean" }, "age": { "type": "integer" }, "posts": { "type": "array", "items": { "$ref": "#/definitions/Post" } } }, "required": [ "name", "isAdmin", "posts" ] } } openapi.json $ tinyspec -–json ajaxy_ru
  • 46. 3 · MODEL SERIALIZATION npmjs.com/package/sequelize-serialize ajaxy_ru
  • 47. 3 · MODEL SERIALIZATION import serialize from 'sequelize-serialize'; router.get('/blog/users', async (ctx) => { const users = await User.findAll({ include: [{ association: User.posts, include: [ Post.comments ] }] }); ctx.body = serialize(users, schemas.UserWithPosts); }); npmjs.com/package/sequelize-serialize ajaxy_ru
  • 50. 4 · STATIC TYPING JSON SCHEMA ajaxy_ru
  • 52. 4 · STATIC TYPING $ tinyspec -–json $ sw2dts ./openapi.json -o Api.d.ts --namespace Api ajaxy_ru
  • 53. 4 · STATIC TYPING $ tinyspec -–json $ sw2dts ./openapi.json -o Api.d.ts --namespace Api declare namespace Api { export interface Comment { authorId: number; message: string; } export interface Post { topic: string; message: string; comments?: Comment[]; } export interface User { name: string; isAdmin: boolean; age?: number; } export interface UserUpdate { name?: string; age?: number; } export interface UserWithPosts { name: string; isAdmin: boolean; age?: number; posts: Post[]; } } Api.d.ts
  • 54. router.patch('/users/:id', async (ctx) => { // Specify type for request data object const userData: Api.UserUpdate = ctx.request.body.user; // Run spec validation await validate(schemas.UserUpdate, userData); // Query the database const user = await User.findById(ctx.params.id); await user.update(userData); // Return serialized result const serialized: Api.User = serialize(user, schemas.User); ctx.body = { user: serialized }; }); 4 · STATIC TYPING ajaxy_ru
  • 55. it('Update user', async () => { // Static check for test input data. const updateData: Api.UserUpdate = { name: MODIFIED }; const res = await request.patch('/users/1', { user: updateData }); // Type helper for request response: const user: Api.User = res.body.user; expect(user).to.be.validWithSchema(schemas.User); expect(user).to.containSubset(updateData); }); 4 · STATIC TYPING router.patch('/users/:id', async (ctx) => { // Specify type for request data object const userData: Api.UserUpdate = ctx.request.body.user; // Run spec validation await validate(schemas.UserUpdate, userData); // Query the database const user = await User.findById(ctx.params.id); await user.update(userData); // Return serialized result const serialized: Api.User = serialize(user, schemas.User); ctx.body = { user: serialized }; }); ajaxy_ru
  • 57. 5 · TYPE CASTING param1=value&param2=777&param3=false Query params or non-JSON body: ajaxy_ru
  • 58. 5 · TYPE CASTING param1=value&param2=777&param3=false { param1: 'value', param2: '777', param3: 'false' } Query params or non-JSON body: ajaxy_ru
  • 60. import castWithSchema from 'cast-with-schema'; router.get('/posts', async (ctx) => { // Cast parameters to expected types const query = castWithSchema(ctx.query, schemas.PostsQuery); // Run spec validation await validate(schemas.PostsQuery, query); // Query the database const posts = await Post.search(query); // Return serialized result ctx.body = { posts: serialize(posts, schemas.Post) }; }); npmjs.com/package/cast-with-schema 5 · TYPE CASTING ajaxy_ru