What "They" Should Have Told You About API Development

Phil Sturgeon

@philsturgeon

1. Documentation

2. REST or RPC
3. arbitrary advice

Easier than you think

  • DON'T DO IT BY HAND
  • Version control with code
  • API Blueprint 👍🏼
  • RAML / Swagger 🙌🏼
  • Just write YAML or MD
  • Attributes / Data Structures
  • JSON Schema

Iterate, improve, and agree up on your contracts before you write any code!

Document First

Documentation without tests is like code without tests; You're just hoping it works well enough that you don't f**k everything up.

Testing

Broken documentation should fail the build.

Testing

Testing

$ npm install -g dredd

$ dredd init

$ dredd 

info: Beginning Dredd testing...

fail: GET /products/1 duration: 17ms
fail: body: At '/data/attributes/volume' Missing required property: volume

complete: 4 passing, 1 failing, 0 errors, 10 skipped, 15 total
complete: Tests took 274ms
  • Mocks are a fake API, generated from your documentation
  • Share mocks to get integration feedback early
  • Automatically generated by Apiary
  • Free alternative for API Blueprint: drakov

Mocking

Mocking

$ npm install -g drakov

$ drakov -f apiary.apib


$ ngrok http 3000

$ http GET http://xxxxxx.ngrok.io/products --json

1. Documentation

2. REST or RPC
3. arbitrary advice

Don't confuse REST for a metric of quality

Making an API 100% RESTful is hard

Do you actually need REST?

Maybe RPC would be more appropriate?

RPC = Commands

REST = Resources

Using commands to interact with resources is awful

 

And vice versa

RPC:   GET /listCheeseburgers
REST: GET /cheeseburgers


RPC:   POST /createCheeseburger
REST: POST /cheeseburgers


RPC:   POST /updateCheeseburger
REST: PATCH /cheeseburgers/1


RPC:   POST /deleteCheeseburger
REST: DELETE /cheeseburgers/1


RPC:   POST /consumeCheeseburger
REST:  urm...?

 

Actions that trigger state changes could be RPC...

... but maybe you could PATCH on fields with a state machine?

module Api
  module States
    class Trip
      include Statesman::Machine

      state :locating, initial: true
      state :in_progress
      state :complete
      state :canceled

      transition from: :locating, to: [:in_progress, :canceled]
      transition from: :in_progress, to: [:complete, :canceled]

      after_transition(from: :locating, to: :in_progress) do |trip|
        trigger_some_action(trip)
      end

      # ...

    end
  end
end

There is a reason Slack use RPC...

So probably RPC For commands then?

RPC: Remote Procedure call

  • Do this random thing
  • Here's some stuff to help you do that thing
  • Poking a black box
  • Great for events/commands/etc
  • Kinda like a stored procedure

REST: representational state transfer

  • Hard to be entirely RESTful
  • Some folks try too hard
  • Can also do actions, but as afterthoughts
  • If RPC is like a stored procedure, REST is like an ORM
  • Without HATEOAS you just have a very pretty RPC API

1. Documentation

2. REST or RPC
3. arbitrary advice

Maybe you don't need HATEOAS

Hypermedia helps your API outlive your startup

TDD is the easiest way to build a complex HTTP API

Describe-it syntax helps you write complex tests easily

RSpec.describe 'Avatars' do
  let(:user) { create(:user) }

  describe 'POST /avatars' do
    let(:png_path) { 'spec/fixtures/files/avatar.png' }
    let(:png_file) { File.read(Rails.root.join(png_path)) }

    context 'when the user already had an avatar' do
      let!(:old_avatar) { create(:avatar, user: user ) }

      it 'deletes the old avatar for the user. No soft delete.' do
        payload = {
          avatars: {
            image_url: 'http://fake-ride/example.png'
          }
        }

        post '/avatars', payload
        expect(user.reload.avatar).not_to eql(old_avatar)
      end
    end
  end
end
RSpec.describe 'Avatars' do
  describe 'POST /avatars' do

    context 'direct image uploads' do
      it 'will fail if content-type is invalid' do
        headers = oauth_headers(user_token).merge({
          'CONTENT_TYPE' => 'some/crap'
        })

        post '/avatars', nil, headers

        expected_response = {
          'errors' => [{
            'code'      => 11_506,
            'title'     => "The root key is missing from the payload.",
            'status'    => 400,
            'details'   => 'avatars'
          }]
        }

        expect(parsed_response).to eq(expected_response)
        expect(response).to have_http_status(:bad_request)

      end
    end
  end
end

Don't let clients hit staging for dev

?include=literally,eve

rything,in,the,goddam,database,what,is,happening,so,slow,help,me,database,server,is,on,fire

apisyouwonthate.com

Made with Slides.com