Upgrade to Pro — share decks privately, control downloads, hide ads and more …

OSCON 2017 - Contract-First API Development Using the OpenAPI Specification (fka Swagger)

OSCON 2017 - Contract-First API Development Using the OpenAPI Specification (fka Swagger)

Dave Forgac

May 09, 2017
Tweet

More Decks by Dave Forgac

Other Decks in Technology

Transcript

  1. Contract-first API
    development
    using the OpenAPI Specification
    (fka Swagger)

    View Slide

  2. Ian Zelikman
    @izcoder
    Dave Forgac
    @tylerdave

    View Slide

  3. Questions

    View Slide

  4. BetterAPIs.com
    https://github.com/tylerdave/OpenAPI-Tutorial/

    View Slide

  5. Updates
    Get repo updates
    git pull
    Reprovision VM
    vagrant suspend
    vagrant reload --provision
    Log in
    vagrant ssh

    View Slide

  6. Backup Plan
    http://editor.swagger.io/

    View Slide

  7. Background

    View Slide

  8. REST

    View Slide

  9. REST
    Standard

    View Slide

  10. REST
    Standard
    Architectural Style

    View Slide

  11. REST
    Standard
    Architectural Style
    HTTP w/ Constraints

    View Slide

  12. REST
    Standard
    Architectural Style
    HTTP w/ Constraints
    REST-inspired HTTP APIs

    View Slide

  13. API Contract

    View Slide

  14. API Contract
    Client ↔ Provider

    View Slide

  15. API Contract
    Client ↔ Provider
    Interface Specification

    View Slide

  16. API Contract
    Client ↔ Provider
    Interface Specification
    SLA, ToS, Limits, Pricing, etc.

    View Slide

  17. JSON

    View Slide

  18. JSON
    JavaScript Object Notation

    View Slide

  19. JSON
    JavaScript Object Notation
    {
    "things": [
    "foo",
    "bar"
    ],
    "message": "Hello, World!"
    }

    View Slide

  20. JSON Schema

    View Slide

  21. JSON Schema
    {
    "title": "Example Schema",
    "type": "object",
    "properties": {
    "displayName": {
    "type": "string"
    },
    "age": {
    "description": "Age in years",
    "type": "integer",
    "minimum": 0
    }
    },
    "required": ["firstName", "lastName"]
    }

    View Slide

  22. YAML

    View Slide

  23. YAML
    Serialization format

    View Slide

  24. YAML
    Serialization format
    (More) human-readable

    View Slide

  25. YAML
    Serialization format
    (More) human-readable
    Superset of JSON

    View Slide

  26. YAML
    Serialization format
    (More) human-readable
    Superset of JSON
    Language Support

    View Slide

  27. YAML Basics - Lists
    ---
    colors:
    - red
    - green
    - blue
    ...

    View Slide

  28. YAML Basics - Dictionaries
    ---
    session:
    title: Contract-First API Development
    type: tutorial
    ...

    View Slide

  29. YAML Basics - Spanning
    ---
    description: |
    This is a long description
    using a pipe
    which will preserve newlines.
    description2: >
    This is a long desciption using > which
    will ignore
    new
    lines.
    ...

    View Slide

  30. YAML Basics - Nesting
    ---
    session:
    name: Contract-First API Development
    type: tutorial
    topics:
    - apis
    - openapi specification
    - swagger
    languages: ['java', 'nodejs', 'python']
    description: >
    A really useful tutorial during which you'll
    learn about API specifications and stuff.
    ...

    View Slide

  31. JSON
    "Talk": {
    "type": "object",
    "properties": {
    "id": {
    "type": "integer"
    },
    "title": {
    "minLength": 1,
    "type": "string",
    "maxLength": 144
    }
    }
    }
    YAML
    Talk:
    type: object
    properties:
    id:
    type: integer
    title:
    type: string
    minLength: 1
    maxLength: 144
    Compare

    View Slide

  32. API Definitions

    View Slide

  33. API Definitions
    WSDL / WADL

    View Slide

  34. API Definitions
    WSDL / WADL
    Swagger -> OpenAPI Spec

    View Slide

  35. API Definitions
    WSDL / WADL
    Swagger -> OpenAPI Spec
    API Blueprint

    View Slide

  36. API Definitions
    WSDL / WADL
    Swagger -> OpenAPI Spec
    API Blueprint
    RAML

    View Slide

  37. OpenAPI Spec

    View Slide

  38. OpenAPI Spec
    Structure

    View Slide

  39. OpenAPI Spec
    Structure
    History

    View Slide

  40. OpenAPI Spec
    Structure
    History
    Future

    View Slide

  41. OpenAPI 3.0

    View Slide

  42. OpenAPI 3.0
    Coming Soon

    View Slide

  43. OpenAPI 3.0
    Coming Soon
    Tooling to Follow

    View Slide

  44. This Tutorial

    View Slide

  45. Goals

    View Slide

  46. Goals
    OpenAPI Spec

    View Slide

  47. Goals
    OpenAPI Spec
    Testing

    View Slide

  48. Goals
    OpenAPI Spec
    Testing
    Mock

    View Slide

  49. Goals
    OpenAPI Spec
    Testing
    Mock
    Basic Implementation

    View Slide

  50. Goals
    OpenAPI Spec
    Testing
    Mock
    Basic Implementation
    Documentation

    View Slide

  51. Repo Layout
    ├── implementation
    │ └── ...
    ├── lessons
    │ ├── lesson-1.01
    │ │ ├── default_broken.yaml
    │ │ └── solution.yaml
    │ ├── lesson-1.02
    │ │ ├── example.json
    │ ... ...
    │ ├── lesson-2.01
    │ │ └── README.md
    │ └── ...
    ├── presentation
    │ └── ...
    └── work

    View Slide

  52. Synced Folder
    Repo dir on host
    mapped to
    /home/ubuntu/tutorial-repo/ on VM

    View Slide

  53. Lessons
    Instructions
    Work in work directory
    Via editor on host machine
    Or via editor in VM terminal
    Save betterapis.yml
    Run validator within VM:
    swagger validate tutorial-repo/work/betterapis.yml

    View Slide

  54. Lesson Solutions
    Done?
    Compare with contents of
    lessons/lesson-x.xx/solution.xxx
    Stuck?
    Copy lessons/lesson-x.xx/solution.xxx to work

    View Slide

  55. Tutorial

    View Slide

  56. Lesson 1.01: Setup
    Goals
    Explore the environment.
    Look at some Open API example specs and
    exercise the tools we will use.

    View Slide

  57. Lesson 1.01: Setup
    Tooling
    Swagger editor:
    http://localhost:8000/
    Validator:
    swagger validate tutorial-repo/lessons/lesson-1.02/solution.yaml

    View Slide

  58. Lesson 1.01: Setup
    Exercise Instructions
    Load several examples from the swagger editor,
    review them.
    Import the broken examples from lesson-1.01
    directory. Try fixing the errors.

    View Slide

  59. Solution 1.01
    Notes
    Got familiar with basic OpenAPI Spec structure

    View Slide

  60. Lesson 1.02: Hello, World!
    Goals
    Building a first (basic) spec.

    View Slide

  61. YAML Example

    View Slide

  62. JSON Example

    View Slide

  63. Lesson 1.02: Hello, World!
    Exercise Instructions
    Build an API for a conference called betterapis
    Include metadata as shown in the example
    Paths are empty for now

    View Slide

  64. Solution 1.02
    Notes
    This solution might be a bit different than yours in
    regards to the metadata.
    Valid Spec!

    View Slide

  65. Lesson 1.03: Pets
    Goals
    Get familiar with defining paths.

    View Slide

  66. Lesson 1.03: Pets
    Basic Path
    /pets:
    get:
    summary: Get a list of pets
    description: Retrieve a list of pets
    responses:
    201:
    description: OK

    View Slide

  67. Lesson 1.03: Pets
    Exercise Instructions
    Add two paths to the API: /talks /speakers .
    Both paths only support GET and only return
    status code 200.

    View Slide

  68. Solution 1.03
    Notes
    We defined the very basic fields and objects
    needed for a valid path.

    View Slide

  69. Lesson 1.04: Registration
    Goals
    Learn to define complex operations on the API.

    View Slide

  70. Lesson 1.04: Registration
    Paths, Actions
    paths:
    /pets:
    post:
    summary: Add pet to DB
    description: Results in new pet information added to the DB
    parameters:
    - name: pet
    in: body
    description: Pet details
    schema:
    required: [name, status]
    properties:
    name:
    type: string
    description: The pet name

    View Slide

  71. Lesson 1.04: Registration
    Paths, Actions (contd.)
    responses:
    201:
    description: Created new pet in the database
    schema:
    required: [pet-id]
    properties:
    pet-id:
    type: number
    description: Unique Id for the pet in the system

    View Slide

  72. Lesson 1.04: Registration
    Exercise Instructions
    Add actions to support speaker registration and
    talk submission
    You are free to define the speaker and talk objects
    as you like as long as you define a unique id in
    both and exercise defining more than one basic
    type for the object properties.

    View Slide

  73. Solution 1.04
    Notes
    Additional properties: readOnly , format , pattern

    View Slide

  74. Lesson 1.05: The Minimalist API
    Goals
    Reusing definitions.
    Learn more in depth about action objects and
    request parameters.

    View Slide

  75. Lesson 1.05: The Minimalist API
    Path Parameter
    /pets/{pet-id}:
    parameters:
    pet-id:
    name: pet-id
    in: path
    description: Pet identifier
    type: number
    required: true

    View Slide

  76. Lesson 1.05: The Minimalist API
    Parameter Reuse
    /pets/{pet-id}:
    parameters:
    - $ref: '#/parameters/pet-id'
    ...
    parameters:
    pet-id:
    name: pet-id
    in: path
    description: Pet identifier
    type: number
    required: true

    View Slide

  77. Lesson 1.05: The Minimalist API
    Definitions Reuse
    ...
    schema:
    $ref: '#/definitions/Pet'
    ...
    definitions:
    Pet:
    type: object
    required: [name, status]
    properties:
    ...
    Pets:
    type: array
    items:
    $ref: "#/definitions/Pet"

    View Slide

  78. Lesson 1.05: The Minimalist API
    Exercise Instructions
    1. Refactor your API to use Talk and Speaker
    objects. Define Talks and Speakers objects based
    on the previous and update the responses from
    /speakers and /talks paths.
    2. Add a two new paths /speakers/{speaker-id} and
    /talks/{talk-id} . Define all the CRUD operations for
    them and use parameter definition outside of the
    action for path parameter.

    View Slide

  79. Solution 1.05
    Notes
    Time saving with definition. More readable.
    Example response in the solution

    View Slide

  80. Lesson 1.06: Responses
    Goals
    Learn more about parameter definition via
    pagination
    Learn How to define reusable responses
    Default responses

    View Slide

  81. Lesson 1.06: Responses
    Pagination
    parameters:
    - $ref: '#/parameters/page-size'
    - $ref: '#/parameters/page-number'
    ...
    parameters:
    page-size:
    name: page-size
    in: query
    description: Number of items
    type: integer
    format: int32
    minimum: 1
    maximum: 100
    multipleOf: 10
    default: 10

    View Slide

  82. Lesson 1.06: Responses
    Response Definition
    responses:
    ServerErrorResponse:
    description: Server error during request.
    schema:
    $ref: "#/definitions/Error"
    definitions:
    Error:
    properties:
    code:
    type: integer
    message:
    type: string

    View Slide

  83. Lesson 1.06: Responses
    Default Response
    /pets/{pet-id}/
    delete:
    responses:
    ...
    default:
    $ref: '#/responses/UnknownResponse'
    responses:
    UnknownResponse:
    description: This response is not yet documented by this API.

    View Slide

  84. Lesson 1.06: Responses
    Exercise Instructions
    Add pagination to the /talks and /speakers paths.
    Pagination should be included by at least two
    parameters: page-size , page-number .
    Add the following responses to all paths: 400, 500,
    default.

    View Slide

  85. Solution 1.06
    Notes
    Functionality complete API

    View Slide

  86. Lesson 1.07: Secure Your APIs
    Goals
    Learn the different security schemas supported.
    Global vs. local security via file upload definition
    example.

    View Slide

  87. Lesson 1.07: Secure Your APIs
    Basic Auth
    securityDefinitions:
    type: basic

    View Slide

  88. Lesson 1.07: Secure Your APIs
    API Key
    securityDefinitions:
    "type": "apiKey",
    "name": "api_key",
    "in": "header"

    View Slide

  89. Lesson 1.07: Secure Your APIs
    OAuth2
    securityDefinitions:
    OauthSecurity:
    type: oauth2
    flow: accessCode
    authorizationUrl: 'https://oauth.swagger.io.com/authorization'
    tokenUrl: 'https://oauth.swagger.io/token'
    scopes:
    admin: Admin scope
    user: User scope
    security:
    - OauthSecurity:
    - user

    View Slide

  90. Lesson 1.07: Secure Your APIs
    File Upload
    paths:
    /pets/{pet-id}/picture:
    parameters:
    - $ref: '#/parameters/pet-id'
    post:
    description: Admin operation to upload a pet picture
    operationId: UploadPicture
    security:
    - OauthSecurity:
    - admin
    consumes:
    - multipart/form-data
    parameters:
    - name: picture
    in: formData

    View Slide

  91. Lesson 1.07: Secure Your APIs
    Exercise Instructions
    Define a security scheme for your API. Use Oauth2.
    Add a new path to be able to upload speaker
    resume and secure it using admin role.

    View Slide

  92. Solution 1.07
    Notes
    Security representation in the editor
    Header in responses

    View Slide

  93. Lesson 1.08: Doc the Docs
    Goals
    Learn additional points on spec documentation

    View Slide

  94. Lesson 1.08: Doc the Docs
    OperationId
    /pets:
    get:
    operationId: GetPets

    View Slide

  95. Description GFM
    /pets:
    get:
    description: ## Retrieve multiple pet objects.
    For example:
    - pet1
    - pet2

    View Slide

  96. Lesson 1.08: Doc the Docs
    Tags
    paths:
    /pets:
    get:
    tags:
    - pet
    ...
    tags:
    name: pet
    description: Pet operations

    View Slide

  97. Lesson 1.08: Doc the Docs
    Exercise Instructions
    Update for API with more information on the
    operations description, using GFM.
    Add tags and operationIds to all your operations

    View Slide

  98. Solution 1.08
    Notes
    Tags in the editor

    View Slide

  99. Lesson 1.09: Can We Split This?
    Goals
    Learn how to support not having all the API in one
    flat file

    View Slide

  100. Lesson 1.09: Can We Split This?
    Reference External Files
    /pets:
    get:
    summary: Get a list of pets
    description: Retrieve a list of pets
    operationId: GetPets
    parameters:
    - $ref: 'parameters.yaml#/page-size'
    - $ref: 'parameters.yaml#/page-number'

    View Slide

  101. Lesson 1.09: Can We Split This?
    parameters.yaml
    Parameters:
    page-size:
    name: page-size
    in: query
    description: Number of items
    type: integer
    format: int32
    minimum: 1
    maximum: 100
    multipleOf: 10
    default: 10

    View Slide

  102. Lesson 1.09: Can We Split This?
    Serving External Files

    View Slide

  103. Lesson 1.09: Can We Split This?
    Exercise Instructions
    Split your API spec. The proposed scheme is to
    have separate file for definitions, parameters and
    responses. You can consider other split strategies.

    View Slide

  104. Solution 1.09
    Notes
    A better-organized specification

    View Slide

  105. Part 1 Recap
    1.01: Setup
    1.02: Hello, World!
    1.03: Pets
    1.04: Registration
    1.05: The Minimalist API
    1.06: Responses
    1.07: Secure Your APIs
    1.08: Doc the Docs
    1.09: Can We Split This?

    View Slide

  106. Break

    View Slide

  107. Contract-first API
    development
    using the OpenAPI Specification
    (fka Swagger)
    Part 2

    View Slide

  108. What do we get?

    View Slide

  109. What do we get?

    View Slide

  110. Benefits

    View Slide

  111. Benefits
    Documentation

    View Slide

  112. Benefits
    Documentation
    Mocking

    View Slide

  113. Benefits
    Documentation
    Mocking
    Testing

    View Slide

  114. Benefits
    Documentation
    Mocking
    Testing
    Code Generation

    View Slide

  115. Benefits
    Documentation
    Mocking
    Testing
    Code Generation

    View Slide

  116. Code Generation

    View Slide

  117. Code Generators

    View Slide

  118. Code Generators
    Servers

    View Slide

  119. Code Generators
    Servers
    Clients

    View Slide

  120. Code Generators
    Servers
    Clients
    Documentation

    View Slide

  121. Swagger-Codegen

    View Slide

  122. Swagger-Codegen
    Via Swagger editor
    Calls to https://generator.swagger.io/

    View Slide

  123. Swagger-Codegen
    Via Swagger editor
    Calls to https://generator.swagger.io/
    Via CLI
    http://swagger.io/swagger-codegen/

    View Slide

  124. Integrated
    Frameworks

    View Slide

  125. Integrated
    Frameworks
    Swagger Inflector (Java)

    View Slide

  126. Integrated
    Frameworks
    Swagger Inflector (Java)
    swagger-node (Node.js)

    View Slide

  127. Integrated
    Frameworks
    Swagger Inflector (Java)
    swagger-node (Node.js)
    Connexion (Python)

    View Slide

  128. Lesson 2.01: Code Generation
    Goals
    Server/Client Code from Spec

    View Slide

  129. Generate Server

    View Slide

  130. Generate Client

    View Slide

  131. Lesson 2.01: Code Generation
    Exercise Instructions
    Generate server & client side code with your
    favorite option provided by the code generator.
    (bonus) Update server side code so that the /talks
    and /speakers paths return empty list on GET. Use
    the methods provided by the client code in order
    test the responses from the server.

    View Slide

  132. Solution 2.01
    Notes
    Experimented with code generated

    View Slide

  133. Connexion

    View Slide

  134. Connexion

    View Slide

  135. Connexion
    Python + Flask

    View Slide

  136. Connexion
    Python + Flask
    Spec As Configuration

    View Slide

  137. Connexion
    Python + Flask
    Spec As Configuration
    Routing, Validation, etc.

    View Slide

  138. Connexion
    Python + Flask
    Spec As Configuration
    Routing, Validation, etc.

    View Slide

  139. Explicit Routing

    View Slide

  140. Explicit Routing
    Explicit Function Name
    paths:
    /hello_world:
    post:
    operationId: myapp.api.hello_world

    View Slide

  141. Explicit Routing
    Explicit Function Name
    paths:
    /hello_world:
    post:
    operationId: myapp.api.hello_world
    Separate Controller Name
    paths:
    /hello_world:
    post:
    x-swagger-router-controller: myapp.api
    operationId: hello_world

    View Slide

  142. Automatic Routing

    View Slide

  143. Automatic Routing
    from connexion.resolver import RestyResolver
    app = connexion.FlaskApp(__name__)
    app.add_api('swagger.yaml', resolver=RestyResolver('api'))

    View Slide

  144. Automatic Route Resolution
    paths:
    /:
    get:
    # Implied operationId: api.get
    /foo:
    get:
    # Implied operationId: api.foo.search
    post:
    # Implied operationId: api.foo.post
    '/foo/{id}':
    get:
    # Implied operationId: api.foo.get
    put:
    # Implied operationId: api.foo.put
    copy:
    # Implied operationId: api.foo.copy
    delete:
    # Implied operationId: api.foo.delete

    View Slide

  145. Request Validation

    View Slide

  146. Request Validation
    JSON Schema

    View Slide

  147. Request Validation
    JSON Schema
    Required parameters

    View Slide

  148. Request Validation
    JSON Schema
    Required parameters
    Types and Formats

    View Slide

  149. Request Validation
    JSON Schema
    Required parameters
    Types and Formats
    Custom Validators

    View Slide

  150. Request Validation
    JSON Schema
    Required parameters
    Types and Formats
    Custom Validators
    HTTP 400 w/ Details

    View Slide

  151. Response Handling

    View Slide

  152. Response Handling
    Serialization

    View Slide

  153. Response Handling
    Serialization
    JSON Encoder

    View Slide

  154. Response Handling
    Serialization
    JSON Encoder
    Validation Optional

    View Slide

  155. Response Handling
    Serialization
    JSON Encoder
    Validation Optional
    Custom Validators

    View Slide

  156. Security

    View Slide

  157. Security
    OAuth 2 via Spec

    View Slide

  158. Security
    OAuth 2 via Spec
    DIY
    API Key
    Basic Auth

    View Slide

  159. Other Features

    View Slide

  160. Other Features
    Swagger UI

    View Slide

  161. Other Features
    Swagger UI
    Swagger JSON

    View Slide

  162. Other Features
    Swagger UI
    Swagger JSON
    Flask Integration

    View Slide

  163. Other Features
    Swagger UI
    Swagger JSON
    Flask Integration

    View Slide

  164. Lesson 2.02: Run the API
    Goals
    Python/Flask
    Connexion

    View Slide

  165. Connexion
    Implementation

    View Slide

  166. Lesson 2.02: Run the API
    Exercise Instructions
    Run the the betterapis application using the
    connexion implementation
    Activate virtualenv: workon tutorial
    Run app: python -m betterapis
    Register two speakers and submit a talk for each
    one.
    Use HTTP POSTs via Postman, curl, et al.
    Request speaker list to verify data persisted.

    View Slide

  167. Solution 2.02
    Notes
    Populated data

    View Slide

  168. Lesson 2.03: Mock Server
    Goals
    Run mock server for client to experiment with the
    API

    View Slide

  169. Lesson 2.03: Mock Server
    examples
    responses:
    200:
    description: Returns a specific talk
    schema:
    $ref: '#/definitions/Pet'
    examples:
    application/json:
    {
    id: 12345,
    name: "pythagoras",
    status: "Adopted"
    }

    View Slide

  170. Lesson 2.03: Mock Server
    Usage
    connexion run betterapis.yaml --mock=all -v

    View Slide

  171. Lesson 2.03: Mock Server
    Exercise Instructions
    Update responses to have examples
    Run and test your mock server

    View Slide

  172. Solution 2.03
    Notes
    Can mock with any spec

    View Slide

  173. Lesson 2.04: Test with Dredd
    Goals
    Learn how the spec connects tests and
    implementation

    View Slide

  174. Lesson 2.04: Test with Dredd
    Installation
    Using npm
    Provided in the VM

    View Slide

  175. Lesson 2.04: Test with Dredd
    Usage
    Dredd init
    ? Location of the API description document
    tutorial-repo/implementation/betterapis/specs/betterapis.yaml
    ? Command to start API backend server e.g. (bundle exec rails server)
    ? URL of tested API endpoint http://127.0.0.1:8080
    ? Programming language of hooks python
    ? Do you want to use Apiary test inspector? No
    ? Dredd is best served with Continuous Integration.
    Create CircleCI config for Dredd? No
    Configuration saved to dredd.yml
    Run test now, with:
    $ dredd

    View Slide

  176. Lesson 2.04: Test with Dredd
    How Dredd works
    In request: x-example or default
    In response: format matching

    View Slide

  177. Lesson 2.04: Test with Dredd
    Parameter Example
    pet-id:
    name: pet-id
    in: path
    description: Pet identifier
    type: number
    required: true
    x-example: 42

    View Slide

  178. Lesson 2.04: Test with Dredd
    Exercise Instructions
    Update all the GET actions so that they can be
    tested using Dredd.
    Initialize dredd and run dredd --method GET
    command in order to verify that tests are passing.
    (Use ids from data you initialized in the application
    in previous lesson)

    View Slide

  179. Solution 2.04
    Notes
    4 tests passed and 6 skipped.

    View Slide

  180. Lesson 2.05: New Features
    Goals
    Development cycle for new feature

    View Slide

  181. Lesson 2.05: New Features
    Flow
    Update the spec
    Run tests -> Fail
    Update the code
    Run tests -> Success

    View Slide

  182. New Feature Demo

    View Slide

  183. Lesson 2.05: New Features
    Exercise Instructions
    Add a new feature to the application to support
    reviews. Besides a unique id the review object
    should reference the talk_id it refers to and
    details .

    View Slide

  184. Solution 2.05
    Notes
    Connecting it all together

    View Slide

  185. Lesson 2.06: Documentation
    Goals
    Generating automatic documentation for clients

    View Slide

  186. Lesson 2.06: Documentation
    Tooling
    Connexion: HTML, Console
    swagger-ui
    Many others

    View Slide

  187. Connexion Docs
    Demo

    View Slide

  188. Lesson 2.06: Documentation
    Exercise Instructions
    Register a new speaker and submit a talk using the
    connexion UI.
    Use the UI to also update and delete the talk and
    the speaker.

    View Slide

  189. Solution 2.06
    Notes
    Easy to distribute documentation

    View Slide

  190. Part 2 Recap
    2.01: Code Generation
    2.02: Run the API
    2.03: Mock Server
    2.04: Test with Dredd
    2.05: New Features
    2.06: Documentation

    View Slide

  191. More Resources
    BetterAPIs.com

    View Slide

  192. Thank You!

    View Slide

  193. Ian
    [email protected]
    @izcoder
    Thank You!
    Questions?
    Dave
    [email protected]
    @tylerdave

    View Slide