Welcome to the MBARI REST Design Guide

This document provides a guide to writing REST services at MBARI. Since REST is not a standard, it is left to each developer to decide how to structure their REST APIs. This is a compilation of best practices that MBARI follows in designing its REST APIs.

  • Use JSON as preferred transport (See the JSON document)
  • Resources are nouns, not verbs
    • Do not add behavior to URLs (i.e. getAccount, getAccountById), use HTTP verbs for state changes
  • Two types of resources
    • Collection (i.e. /accounts)
    • Instance (i.e. /accounts/uuid)
  • Use UUIDs for instance identifiers (allows for distributed and clustered services)
  • Use HTTP verbs for behavior
    • GET - get a resource
    • POST - Create. By spec, this is idempotent so the entire object must be specified with POST. Partial updates are what PATCH is designed for.
    • PUT - used for updates. Only verb that is not idempotent. Can be used on parent to create a new child.
    • DELETE - delete a resource
    • HEAD - just get headers, no body
    • PATCH - update a resource
  • Media-Types
    • You can create your own media types using something like application/foo+json which means the response from the server is JSON, but it is associated with 'foo' which can have information associated with it (schema, parsing rules, etc.). You can even register these type with standards body IANA.
  • Clients should send Accept header and put them in order of preference that they would like to get them back.
  • Responses
    • Set Content-Type on response
    • Create or updates (POST, PUT, PATCH) should return code 201 with the URL of the instance in the 'Location' header.
    • Resource should be returned with most HTTP verbs, but give the client a parameter of _body=false to turn off the returning of the resource.
  • URL design
    • Versioning: Even though specifying version in Media-Type is idealistic way (i.e. application/foo+json&v=1), URL versioning is more pragmatic (i.e https://api.mbari.org/v1)
    • Don't redirect clients if they come from a browser, developers learn and test using a browser
    • Emphasis should be on Media-Types, not URLs
    • Client can request format of response using extensions and they override the Accept header. (e.g. /accounts/uuid.json)
  • Resource Format
    • Use CamelCase (JavaScript and JSON use it, makes the most sense)
    • For dates:
      • use ISO 8601
      • USE UTC!!!!
    • Every resource has a unique URL, use the URL NOT just and ID
    • Use resource references for linking as it is paramount for scalability.
    • Give the clients a way to expand the links using expand parameter. For example: &expand=directory tells the server to expand the directory property on the resource that was requested. The server should then materialze the link, but keep the href ID.
    • Give the client a means to request partial representations. Do this by having the client include a list of fields they want returned. For example: &fields=givenName,surname,directory will return only those fields in the resource
    • If you have lots of results, implement pagination
      • Use offset and limit parameters (i.e. /accounts?offset=50&limit=25)
      • Embed that information in the response (see example below)
    • Many-Many relationships
      • Each many to many mapping is a resources in and of itself. For example, if you have groups and accounts that are many to many, you can use something like groupMembership to represent that (see example).
      • This allows for metadata to be hung on the relationship itself (like when it was created, etc.)
      • The relationship resource can be shown in the instance like any other property with an href.
  • Errors
    • Be as descriptive as possible
    • Tell them what went wrong and how to fix it
  • Security
    • Avoid sessions when possible
      • authenticate every request if necessary
    • Authorize based on resource content, NOT URL!
    • Use Oauth 2
      • Must be in HTTP Header. For example: Authorization: Bearer {JWT}
    • Can use API keys instead of bearer tokens, they are good because:
      • can use entropy to create them, harder to crack
      • can separate password reset from API key expiration
      • Independence
      • Speed
      • Limited Exposure
        • The api key should be downloaded and put in a directory on the client that only the client account can read
      • Traceability
      • Must be in HTTP Header. For example: 'Authorization: ApiKey {API KEY}'
      • Note the API Key itself can be a JWT.
    • 401 vs 403
      • 401 really means “unauthenticated"
      • 403 really means “unauthorized"
    • HTTP Authentication Schemes
      • response from server to issue challenge:
        • WWW-Authenticate: \<scheme name>
        • realm=“Application Name"
      • client request to submit credentials in header:
        • Authorization: \<scheme name> \<data>
    • We are using Auth0 for authentication at MBARI

Examples

Request

GET /accounts/uuid

Response (directory is one-one and groups is one-many)

{
   "code": 200,
   "apiVersion": "v1.0.1",
   "data": {
       "kind":"accounts",
       "fields":"href,givenName,surname,directory,groups",
       "selfLink":"https://api.mbari.org/v1/accounts/account-uuid",    
       "items": [{
           "href":"https://api.mbari.org/v1/accounts/account-uuid",
           "givenName":"Tony",
           "surname":"Stark",
           "directory": {
               "href": "https://api.mbari.org/v1/directories/directory-uuid"
           },
           "groups": {
               "href": "https://api.mbari.org/v1/accounts/account-uuid/groups"
           }
       }]
   }
}

Pagination

Request

GET /accounts?offset=0&limit=25

Response

{
   "code": 200,
   "apiVersion": "v1.0.1",
   "data": {
       "kind":"accounts",
       "fields":"href,givenName,surname,directory,groups",
       "itemsPerPage":25,
       "startIndex":0,
       "nextLink": {"href":"https://api.mbari.org/v1/accounts?offset=25&limit=25"},
       "previousLink":null,
       "selfLink":"https://api.mbari.org/v1/accounts?offset=0&limit=25",
       "items": [
           {
               "href":"https://api.mbari.org/v1/accounts/account-uuid",
               "givenName":"Tony",
               "surname":"Stark",
               "directory": {
                   "href": "https://api.mbari.org/v1/directories/directory-uuid"
               },
               "groups": {
                   "href": "https://api.mbari.org/v1/accounts/account-uuid/groups"
               }
           },{

           }
       ]
   }
}

Many-Many

Request

GET /groupMemberships/uuid

Response

{
   "code":200,
   "apiVersion":"v1.0.0",
   "data": {
       "kind":"groupMemberships",
       "fields": "href,account,group",
       "selfLink":"https://api.mbari.org/v1/groupMemberships/uuid",
       "items": [
           {
               "href":"https://api.mbari.org/v1/groupMemberships/uuid",
               "account": { "href":"…"},
               "group": {"href":"…"}
           }
       ]
   }
}

Error

{
    "code":409,
    "message":"Unauthorized",
    "errors" : [
        {
            "errorCode": 409245,
            "domain":"mbari.auth0.com",
            "reason":"Invalid Token",
            "message":"There was a token, but it was invalid",
            "extendedHelp":"https://docs.mbari.org/api-name/errors/409245",
            "sendReport":"https://apis.mbari.org/feedback",
            "developerMessage":"This can be caused by an expired token or a malformed token"
        }    
    ]
}