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