Otaqui.com Blog

Digital Ocean should fix their API – here’s how

I love Digital Ocean – they offer great value for money, excellent support and a really refreshing and clean interface. However their API sucks, and this is something they should probably try and deal with if they want to expand beyond individuals or small companies into providing services for larger applications and corporates.

Versioning the API

First off, I think they should version their API . Some people think that REST precludes versioning in a URL but personally I don’t agree. RESTful APIs aid in discovery, but do not make it automatic, and adding a version at least allows concurrent incompatible implementations – especially important when you are still developing a service. The devx article linked to there suggests that “/v1/” or similar in an API URL is a bad idea, but doesn’t say that you can’t provide such versions from subdomains, so I guess that would appease the author.

Let’s suggest then that they could have all API requests to URLs like this:

https://api-1.digitalocean.com/

Allowing them to provide a different API in the future without breaking existing clients, such as:

https://api-2.digitalocean.com/

There are other ways of adding versioning, within the url (i.e. https://api.digitalocean.com/v1/) but that ship has mostly sailed for DO with their current implementation in place, or by requiring extra header information to specify API version in requests.

Be at REST

Assuming it’s now possible to create a new version of the API, let’s look at what that might contain (spoiler: more than only GET requests ).

I’m going to apply a more correctly RESTful style of API using the appropriate HTTP verbs to their current API actions – I’m not going to suggest any new features.

Droplets

Method URL Description
GET /droplets

Show All Active Droplets

As original /droplets

POST /droplets

New Droplet

As original /droplets/new, but current request parameters sent as a JSON post body:

{
  "name": "droplet_name",
  "size": "size_id",
  "image_id": "image_id",
  "region_id": "region_id",
  "ssh_key_ids": ["ssh_key_id1", "ssh_key_id2"],
  "private_networking": true
}
GET /droplets/:droplet_id

Show Droplet

As original /droplets/:droplet_id

GET /droplets/:droplet_id/actions

Get a list of actions available on this server

Lists the actions available, i.e. {"actions": ["power_off", "resize", "password_reset"] etc. Available actions are context-sensitive, i.e. “power_off” is not supplied if the droplet is already powered off.

POST /droplets/:droplet_id/actions

Perform an action on a droplet. The action and parameters are provided in the post body, i.e.:

  • reboot ({"action": "reboot"})
  • power_cycle ({"action": "power_cycle"})
  • shutdown ({"action": "shutdown"})
  • power_off ({"action": "power_off"})
  • power_on ({"action": "power_on"})
  • password_reset ({"action": "password_reset"})
  • resize ({"action": "resize", "size": "size_id"})
  • restore ({"action": "restore", "image_id": "image_id"})
  • rebuild ({"action": "restore", "image_id": "image_id"})
  • backups ({"action": "backups", "backups": true})
  • rename ({"action": "rename", "name":"new name"})
DELETE /droplets/:droplet_id

Destroy a droplet

GET /droplets/:droplet_id/snapshots

List Snapshots

POST /droplets/:droplet_id/snapshots

Create Snapshot

GET /droplets/:droplet_id/snapshots/:snapshot_id

Show Snapshot

(NB – equivalent to GET /images/:snapshot_id)

DELETE /droplets/:droplet_id/snapshots/:snapshot_id

Delete Snapshot

(NB – equivalent to DELETE /images/:snapshot_id)

GET /droplets/:droplet_id/backups

List Backups

GET /droplets/:droplet_id/backups/:backup_id

Show Backup

(NB – equivalent to GET /images/:backup_id)

Regions

Method URL Description
GET /regions

List regions

Images

Method URL Description
GET /images

List Images

PUT /images/:image_id

Transfer Image

Supply {"region_id": "region_id"} in the post body.

DELETE /images/:image_id

Destroy Image

SSH Keys

Method URL Description
GET /ssh_keys

List SSH Keys

POST /ssh_keys

Create SSH Key

GET /ssh_keys/:ssh_key_id

Show SSH Key

PUT /ssh_keys/:ssh_key_id

Update SSH Key

DELETE /ssh_keys/:ssh_key_id

Destroy SSH Key

Sizes

Method URL Description
GET /sizes

List sizes

Domains

Method URL Description
GET /domains

List Domains

POST /domains

Create Domain

GET /domains/:domain_id

Show Domain

DELETE /domains/:domain_id

Destroy Domain

GET /domains/:domain_id/records

List Domain Records

POST /domains/:domain_id/records

Create Domain Record

GET /domains/:domain_id/records/:record_id

Show Domain Record

PUT /domains/:domain_id/records/:record_id

Update Domain Record

DELETE /domains/:domain_id/records/:record_id

Destroy Domain Record

Events

Method URL Description
GET /events/:event_id

Show Event

In this process I’ve tried to take a pragmatic approach to the API. You could easily make the case that to be more theoretically correct a lot of the droplet “actions” should be done with a PUT request to /droplets/:droplet_id and while I think that would work fine, it seemed to me that splitting out the actions to a different URL made things a bit more obviously with less likelihood of messing up with trying to PUT too many things at once.

I do think at least this much work on the API is worthwhile – I’m somewhat terrified of GET requests that perform actions (especially if those actions might cost me money). It’s nice to write scripts against APIs, and a very nice safety belt if you know that you’re only doing (safe) GET requests in a loop or something.

There are a couple of extra features I would like to see DO to implement: