API Design patterns
for the REST of US

php[tek] 2023

Ian Littman / @ian@phpc.social / @iansltx

follow along at https://ian.im/apitek23

Alternate Title:
I Read Roy Fielding's dissertation
So you wouldn't have to

pro tip: start with chapter 5

What we'll cover

  • Principles of REST and what that means for your app
  • The API design process
  • General HTTP API design principles
    • URL Structure
    • Status Codes
    • Versioning
    • Authentication + Security

So...what's this REST thing?

  • Codified by Roy Fielding in his doctoral dissertation
  • An architectural style
  • A set of constraints that make interacting with a web-based system more predictable
  • Describes information structure on the Web, not just APIs
  • REST does not dictate
    • Component implementation
    • Protocol syntax

Constraint #1: Client-Server

Interfaces, not implementations

GET /gyros
Accept: video/mp4
{other headers here}

HTTP/1.1 200 OK
Cache-Control: public, max-age: 3600
Content-Type: video/mp4
Date: Thu, 18 May 2023 16:05:00 GMT
Vary: Accept-Encoding
Content-Length: 20230518
Connection: keep-alive
X-Powered-By: Battle Snake

 

Client-server interactions can/should be streamed

REST Connectors

  • Client (Chrome, curl)
  • Server (Apache, nginx, node)
  • Cache (browser cache, Cloudflare, Varnish)
  • Resolver (DNS)
  • Tunnel (SOCKS, TLS via HTTP CONNECT)

REST Components

  • Origin Server (Apache, nginx + FPM, node)
  • Gateway (network-imposed; Squid, nginx)
  • Proxy (client-selected; SSH tunnel or VPN)
  • User Agent (Chrome, curl)

Constraint #2: Stateless

  • No concept of a "session" (with preferences etc. baked in)
  • Current user ID shouldn't be in URL (impacts caching!)
  • Stateless requests are easier to reason about for cacheability
  • Cookies are an antipattern*
  • JWTs are less of an antipattern
  • WebSockets aren't REST

 

* They may be the best way to pass auth back and forth

Con$traint #3: Cacheable

An interesting observation is that the most efficient network request is one that doesn't use the network.

- Roy Fielding

HTTP/1.1 304 Not Modified
Cache-Control: public, max-age: 3600
ETag: a1234b1234
Content-Type: application/json
Date: Fri, 18 May 2023 16:13:37 GMT
Vary: Accept-Encoding
Content-Length: 42
Connection: keep-alive
X-Powered-By: An Elephpant
If-Modified-Since
If-None-Match
Vary

REST expects caches/proxies inline

  • Browser cache
  • CDN

Cache constraints

  • Only for retrieval
  • Only for the same representation (Vary header)
  • Only unauthenticated (for shared caches)

Constraint #4: Uniform Interface

Since REST is specifically targeted at distributed information systems, it views an application as a cohesive structure of information and control alternatives through which a user can perform a desired task.

- Roy Fielding

Uniform Interface Constraints

  • Identification of resources
  • Manipulation of resources through representations
  • Self-descriptive messages
  • Hypermedia as the engine of application state

Form follows function

An architecture for the web must therefore be designed with the context of communicating large-grain data objects across high-latency networks and multiple trust boundaries.

- Roy Fielding

Why large-grain data objects?

  • Amortizes communication overhead
  • More consistent format allows for more caching

REST Data Elements

  • Resource
  • Resource identifier (URI/URN)
  • Representation
    • HTML document
    • JPEG image
    • MKV video
  • Representation metadata
    • Content-Type
    • Last-Modified
  • Resource Metadata

    • Source link

    • Alternates

    • Vary
  • Control Data

    • If-Modified-Since
    • Cache-Control

A resource is a reference

Not An entity itself

Representations vs. Resources

  • Resource
    • An identifier
    • Defined by URI
    • e.g. /users/5 or /events/upcoming or /talks?user_id=10
    • REST != pretty URLs
    • URIs should change as little as possible
  • Representation
    • Multiple may be available for a given resource
    • Use content-type negotiation
    • e.g. a QR code vs. a text URI for 2FA
Accept
Content-Type
POST /tokens

HTTP/1.1 201 Created
Cache-Control: no-cache, no-store
Content-Type: application/hal+json
Date: Thu, 18 May 2023 16:20:24 GMT
Content-Length: 337
X-Powered-By: COBOL ON COGS 10.11.0
POST /tokens
Accept: image/png

HTTP/1.1 201 Created
Cache-Control: no-cache, no-store
Content-Type: image/png
Date: Thu, 18 May 2023 16:21:26 GMT
Content-Length: 1337
X-Powered-By: Bacon
POST /tokens
Accept: text/xml

HTTP/1.1 406 Not Acceptable
Cache-Control: no-cache, no-store
Date: Thu, 18 May 2023 16:22:28 GMT
Content-Length: 123
X-Powered-By: Fresh Outta Angle Brackets

Hypermedia as the engine of application state

Still an inexact science

  • REST doesn't dictate a particular hypermedia spec
  • Some (e.g. Siren) include information on non-GET actions
  • Some don't
  • Make sure you're consistent

Constraint #5: Layered System

  • Abstraction && Encapsulation (Middleware!)
  • Examples
    • Load balancing
    • Authentication
    • Caching
    • Content Type Transformation
  • Layers should be deployable independently

Constraint #6: Code on Demand

  • Optional
  • Allows for shifting to the client side
    • Processing effort
    • Less-common interaction patterns
  • Examples
    • An SPA or SDK served from the API
    • Codegen'd example via (the former) Symfony Melody
  • Downside: language-specific

Core constraints of REST

  • Client-server
  • Stateless
  • Cacheable
  • Uniform Interface
  • Layered System
  • Code On Demand (optional)

SometimeS full REST isn't the answer

Mix and match (RPC, GraphQL)

...but know why you're picking a tool for the job

DOn't let your API happen by accident

  1. Figure out what workflows your users need to perform
  2. Build an API spec (e.g. OpenAPI), matching workflows to a series of API endpoints
  3. Build docs/mock APIs explaining how to use the API to perform the workflows, see how users react
  4. Build only the API that you've spec'd
  5. Get feedback
  6. Update the API, keeping the API docs/spec in sync
  7. Repeat

Write the docs!

  • Your spec by default isn't enough
  • Outdated docs may be worse than no docs
  • Focus on user flows first
  • Figure out how you're going to provide support

the FIve Minute Rule

...and now for some API-Pinions

URL structure: Plural, not singular

  • Good: /users, /users/1
  • Bad: /user, /user/1
  • Break this rule if you're 100% sure you'll never have more than one of a resource, e.g. /me

URL structure: Nouns, not verbs

  • Good: /users/1/messages
  • Bad: /users/1/sendMessage
  • Also bad: /getUser?id=1
  • Use RPC if you must...
  • ...but try to model interactions as transferring representations of resources first (e.g. creating an event)

URL structure: Don't nest too deeply

  • Good: /poems?tag=bird,lenore&century=19&author=poe
  • Bad: /poems/19/poe/bird
  • Pattern: /:resource/:id/:relation (links to top level IDs)

HTTP Methods, REST verbs

  • GET: Find a thing (or a collection of things), idempotent
  • POST: Create a thing (or, if you must, do an RPC)
  • PATCH: Update a part of a thing
  • PUT
    • Update an entire thing (zero out non-provided fields)
    • Create a thing with a client-specified ID (return a 201!)
  • DELETE: Delete a thing (return a 204 or 205)
  • OPTIONS: Which methods are allowed for this endpoint?
  • Use HTTP 405 (Method Not Allowed) as applicable

HTTP Status Codes

  • Clients should assume x00 if they don't understand the exact code returned
  • 1xx - Provisional
    • 100 - Continue (streamed output)
    • 101 - Upgrade (e.g. to WebSockets, HTTP/2)
  • 2xx - Success
    • 200 - OK (don't put an error here)
    • 201 - Created (can include Location header)
    • 202 - Accepted (for async processing)
    • 203 - Non-authoritative information (caching)
    • 204 - No Content
    • 205 - Reset Content

HTTP Status Codes - 3xx, redirects

  • Location header (except for 304)
  • 300 - Multiple Choices
  • 301/308 - Moved Permanently
  • 302/307 - Found
  • 303 - DONTTRUSTME  See Other
  • 304 - Not Modified

HTTP Status Codes - 4xx, client errors

  • 400/422 - Your request format is bad
  • 401 - Who are you?
  • 402 - Pay me.
  • 403 - I know who you are. You aren't allowed here.
  • 404 - Nothing here.
  • 405 - Wrong Method.
  • 406 - I can't provide that content type.
  • 408 - Timeout
  • 409 - Conflict (e.g. when updating)
  • 410 - Something was here but isn't here now.
  • 418 - I'm a Teapot
  • 429 - Too Many Requests
  • 451 - Unavailable for Legal Reasons

HTTP Status Codes - 5xx, Server errors

  • 500 - Generic server error
  • 501 - Not implemented
  • 502 - The server behind this one is broken
  • 503 - Service Unavailable
  • 504 - The server behind this one timed out

Versioning

  • Clients shouldn't care about semver-minor versions; don't use
  • For BC breaks
    • Path: URL prefixes (/v2)
    • Representation: Content-Type (application/hal+json.v1)
    • Mix of both: custom header (API-Version)
  • Will you require users to specify a version (for header-based)
  • If not, what do you default to?
    • Latest
    • Earliest
    • Account-scoped API version (a la Stripe)

Auth

  • HTTP Basic/API Keys
  • OAuth (we'll talk about OAuth 2)
    • Access Tokens
    • Refresh Tokens
    • Grant Types
      • Client Credentials
      • Auth Code
      • Implicit
      • Device Authorization
      • Password (deprecated in 2.1)

Thanks! Questions?

ian.im/apitek23 - these slides

 

phpc.social/@ian - me!

 

twitter.com/iansltx - also me

 

github.com/iansltx - my code!

 

<- rate this talk