Designing-a-Beautiful-REST+JSON-API
2020-02-23 152浏览
- 1. Beautiful REST+JSON APIs Les Hazlewood @lhazlewood Apache Shiro Project Chair Expert Group Member, JEE Application Security CTO, Stormpath, stormpath.com
- 2. .com • User Management API for Developers • Password security • Authentication and Authorization • LDAP & Active Directory Cloud Sync • Instant-on, scalable, and highly available • Free for developers @lhazlewood
- 3. Outline • APIs, REST & JSON • REST Fundamentals • Design Base URL Versioning Resource Format Return Values Content NegoEaEon References (Linking) PaginaEon Query Parameters AssociaEons @lhazlewood Errors IDs Method Overloading Resource Expansion ParEal Responses Caching & Etags Security MulE Tenancy Maintenance Batch OperaEons
- 4. APIs • • • • • Applications Developers Pragmatism over Ideology Adoption Scale @lhazlewood
- 5. Why REST? • • • • • • Scalability Generality Independence Latency (Caching) Security Encapsulation @lhazlewood
- 6. Why JSON? • • • • • Ubiquity Simplicity Readability Scalability Flexibility @lhazlewood
- 7. HATEOAS • • • • • • • Hypermedia As The Engine Of Application State @lhazlewood
- 8. REST Is Easy @lhazlewood
- 9. REST Is *&@#$! Hard (for providers) @lhazlewood
- 10. REST can be easy (if you follow some guidelines) @lhazlewood
- 11. Example Domain: Stormpath • • • • • • Applications Directories Accounts Groups Associations Workflows
- 12. Fundamentals @lhazlewood
- 13. Resources Nouns, not Verbs Coarse Grained, not Fine Grained Architectural style for use-case scalability @lhazlewood
- 14. What If? /getAccount /createDirectory /updateGroup /verifyAccountEmailAddress @lhazlewood
- 15. What If? /getAccount /getAllAccounts /searchAccounts /createDirectory /createLdapDirectory /updateGroup /updateGroupName /findGroupsByDirectory /searchGroupsByName /verifyAccountEmailAddress /verifyAccountEmailAddressByToken … Smells like bad RPC. DON’T DO THIS. @lhazlewood
- 16. Keep It Simple @lhazlewood
- 17. The Answer Fundamentally two types of resources: Collection Resource Instance Resource @lhazlewood
- 18. Collection Resource /applications @lhazlewood
- 19. Instance Resource /applications/a1b2c3 @lhazlewood
- 20. Behavior • GET • PUT • POST • DELETE • HEAD @lhazlewood
- 21. Behavior POST, GET, PUT, DELETE ≠ 1:1 Create, Read, Update, Delete @lhazlewood
- 22. Behavior As you would expect: GET = Read DELETE = Delete HEAD = Headers, no Body @lhazlewood
- 23. Behavior Not so obvious: PUT and POST can both be used for Create and Update @lhazlewood
- 24. PUT for Create IdenEfier is known by the client: PUT /applications/clientSpecifiedId { … } @lhazlewood
- 25. PUT for Update Full Replacement PUT /applications/existingId { “name”: “Best App Ever”, “description”: “Awesomeness” } @lhazlewood
- 26. PUT Idempotent @lhazlewood
- 27. POST as Create On a parent resource POST /applications { “name”: “Best App Ever” } Response: 201 Created Location: https://api.stormpath.com/applications/a1b2c3 @lhazlewood
- 28. POST as Update On instance resource POST /applications/a1b2c3 { “name”: “Best App Ever. Srsly.” } Response: 200 OK @lhazlewood
- 29. POST NOT Idempotent @lhazlewood
- 30. Media Types • Format SpecificaEon + Parsing Rules • Request: Accept header • Response: Content-Type header • • • • application/json application/foo+json application/foo+json;application … @lhazlewood
- 31. Design Time! @lhazlewood
- 32. Base URL @lhazlewood
- 33. http(s)://foo.io vs http://www.foo.com/dev/service/api/rest @lhazlewood
- 34. http(s)://foo.io Rest Client vs Browser @lhazlewood
- 35. Versioning @lhazlewood
- 36. URL https://api.stormpath.com/v1 vs. Media-Type application/foo+json;application&v=2 application/foo2+json;application @lhazlewood
- 37. Resource Format @lhazlewood
- 38. Media Type Content-Type: application/json When time allows: application/foo+json application/foo2+json;bar=baz … @lhazlewood
- 39. Media Type Don’t go overboard! Media Type != Schema! Most only need 2 or 3 custom media types: • instance resource • collection resource application/foo+json application/foo2+json;bar=baz … @lhazlewood
- 40. camelCase ‘JS’ in ‘JSON’ = JavaScript myArray.forEach Not myArray.for_each account.givenName Not account.given_name Underscores for property/function names are unconventional for JS. Stay consistent. @lhazlewood
- 41. Date/Time/Timestamp There’s already a standard. Use it: ISO 8601 Example: { …, “createdAt”: “2013-07-10T18:02:24.343Z”, ... } Use UTC! @lhazlewood
- 42. createdAt / updatedAt @lhazlewood
- 43. createdAt / updatedAt Most people will want this at some point { …, “createdAt”: “2013-07-10T18:02:24.343Z”, “updatedAt”: “2014-09-29T07:02:48.761Z” } Use UTC! @lhazlewood
- 44. Response Body @lhazlewood
- 45. GET obvious What about POST? Return the representation in the response when feasible. Add override (?_body=false) for control @lhazlewood
- 46. Content Negotiation @lhazlewood
- 47. Header • Accept header • Header values comma delimited • q param determines precedence, defaults to 1, then conventionally by list order GET /applications/a1b2c3 Accept: application/json, text/ plain;q=0.8 @lhazlewood
- 48. Resource Extension /applications/a1b2c3.json /applications/a1b2c3.csv … ConvenEonally overrides Accept header @lhazlewood
- 49. HREF • Distributed Hypermedia is paramount! • Every accessible Resource has a canonical unique URL • Replaces IDs (IDs exist, but are opaque). • Critical for linking, as we’ll soon see @lhazlewood
- 50. Instance w/ HREF (v1) GET /accounts/x7y8z9 200 OK { “href”: “https://api.stormpath.com/v1/accounts/x7y8z9”, “givenName”: “Tony”, “surname”: “Stark”, ... } @lhazlewood
- 51. Resource References aka ‘Linking’ (v1) @lhazlewood
- 52. • Hypermedia is paramount. • Linking is fundamental to scalability. • Tricky in JSON • XML has it (XLink), JSON doesn’t • How do we do it? @lhazlewood
- 53. Instance Reference (v1) GET /accounts/x7y8z9 200 OK { “href”: “https://api.stormpath.com/v1/accounts/x7y8z9”, “givenName”: “Tony”, “surname”: “Stark”, …, “directory”: ???? } @lhazlewood
- 54. Instance Reference (v1) GET /accounts/x7y8z9 200 OK { “href”: “https://api.stormpath.com/v1/accounts/x7y8z9”, “givenName”: “Tony”, “surname”: “Stark”, …, “directory”: { “href”: “https://api.stormpath.com/v1/directories/g4h5i6” } } @lhazlewood
- 55. Collection Reference (v1) GET /accounts/x7y8z9 200 OK { “href”: “https://api.stormpath.com/v1/accounts/x7y8z9”, “givenName”: “Tony”, “surname”: “Stark”, …, “groups”: { “href”: “https://api.stormpath.com/v1/accounts/x7y8z9/groups” } } @lhazlewood
- 56. Linking v2 (recommended) @lhazlewood
- 57. Instance HREF (v2) GET /accounts/x7y8z9 200 OK { “meta”: { “href”: “https://api.stormpath.com/v1/accounts/x7y8z9”, “mediaType”: “application/ion+json”, ... }, “givenName”: “Tony”, “surname”: “Stark”, … } @lhazlewood
- 58. Instance Reference (v2) GET /accounts/x7y8z9 200 OK { “meta”: { ... }, “givenName”: “Tony”, “surname”: “Stark”, …, “directory”: { “meta”: { “href”: “https://api.stormpath.com/v1/directories/g4h5i6” “mediaType”: “application/ion+json” } } } @lhazlewood
- 59. Collection Reference (v2) GET /accounts/x7y8z9 200 OK { “meta”: { ... }, “givenName”: “Tony”, “surname”: “Stark”, …, “groups”: { “meta”: { “href”: “https://api.stormpath.com/v1/accounts/x7y8z9/groups”, “mediaType”: “application/ion+json”, “rel”: [“collection”] } } } @lhazlewood
- 60. Reference Expansion (aka Entity Expansion, Link Expansion) @lhazlewood
- 61. Account and its Directory? @lhazlewood
- 62. GET /accounts/x7y8z9?expand=directory 200 OK { “meta”: {...}, “givenName”: “Tony”, “surname”: “Stark”, …, “directory”: { “meta”: { ... }, “name”: “Avengers”, “description”: “Hollywood’s hope for more $”, “createdAt”: “2012-07-01T14:22:18.029Z”, … } } @lhazlewood
- 63. Partial Representations @lhazlewood
- 64. GET /accounts/x7y8z9? fields=givenName,surname,directory(name) @lhazlewood
- 65. Collections! @lhazlewood
- 66. Collections • • • • A first class resource ‘citizen’ Own href / metadata Own properties Different from all other collections @lhazlewood
- 67. GET /accounts/x7y8z9/groups 200 OK { “meta”: { ... }, “offset”: 0, “limit”: 25, “size”: 289, “first”: { “meta”:{“href”: “…/accounts/x7y8z9/groups?offset=0”}}, “previous”: null, “next”: { “meta”:{“href”: “…/accounts/x7y8z9/groups?offset=25”}}, “last”: { “meta”:{“href”: “…”}}, “items”: [ { “meta”: { “href”: “…”, ...} }, … ] } @lhazlewood
- 68. Pagination @lhazlewood
- 69. Collection Resource supports query params: • Offset • Limit …/applications?offset=50&limit=25 @lhazlewood
- 70. GET /accounts/x7y8z9/groups 200 OK { “meta”: { ... }, “offset”: 0, “limit”: 25, “first”: { “meta”:{“href”: “…/accounts/x7y8z9/groups?offset=0”}}, “previous”: null, “next”: { “meta”:{“href”: “…/accounts/x7y8z9/groups?offset=25”}}, “last”: { “meta”:{“href”: “…”}}, “items”: [ { “meta”: { “href”: “…”, ...} }, { “meta”: { “href”: “…”, ...} }, … ] } @lhazlewood
- 71. Sorting @lhazlewood
- 72. GET .../accounts? orderBy=surname,givenName%20desc @lhazlewood
- 73. Search @lhazlewood
- 74. “Find all accounts with a ‘company.com’ email address that can login to a particular application” @lhazlewood
- 75. GET /applications/x7y8z9/accounts? email=*company.com 200 OK { “meta”: { ... }, “offset”: 0, “limit”: 25, “first”: { “meta”:{“href”: “/applications/x7y8z9/accounts? email=*company.com&offset=0”}}, “previous”: null, “next”: { “meta”:{“href”: “/applications/x7y8z9/accounts? email=*company.com&offset=25”}}, “last”: { “meta”:{“href”: “…”}}, “items”: [ { “meta”: { “href”: “…”, ...} }, { “meta”: { “href”: “…”, ...} }, … ] } @lhazlewood
- 76. Search cont’d • Filter search .../accounts?q=some+value • Attribute Search .../accounts? surname=Joe&email=*company.com @lhazlewood
- 77. Search cont’d • Starts with ?email=joe* • Ends with ?email=*company.com • Contains ?email=*foo* @lhazlewood
- 78. Search cont’d • Range queries “all accounts created between September 1st and the 15th” .../accounts? createdAt=[2014-09-01,2014-09-15] @lhazlewood
- 79. Many To Many @lhazlewood
- 80. Group to Account • A group can have many accounts • An account can be in many groups • Each mapping is a resource: GroupMembership @lhazlewood
- 81. GET /groupMemberships/23lk3j2j3 200 OK { “meta”:{“href”: “…/groupMemberships/23lk3j2j3”}, “account”: { “meta”:{“href”: “…”} }, “group”: { “meta”{“href”: “…”} }, … } @lhazlewood
- 82. GET /accounts/x7y8z9 200 OK { “meta”:{“href”: “…/accounts/x7y8z9”}, “givenName”: “Tony”, “surname”: “Stark”, …, “groups”: { “meta”:{“href”: “…/accounts/x7y8z9/groups”} }, “groupMemberships”: { “meta”:{“href”: “…/groupMemberships?accountId=x7y8z9”} } } @lhazlewood
- 83. Async or Long-Lived Operations @lhazlewood
- 84. POST /emails { “from”: me@somewhere.com, “subject”: “Hi!” “body”: “...” } @lhazlewood
- 85. 204 Accepted Location: /emails/23Sd932sSl { “status”: “queued”, ... } @lhazlewood
- 86. GET /emails/23Sd932sSl Expires: 2014-09-29T18:00:00.000Z { “status”: “sent”, ... } @lhazlewood
- 87. Batch Operations @lhazlewood
- 88. • Each batch reflects a resource • Batches are likely to be a collection • Batches are likely to have a status • Batch deletes easier than create/update @lhazlewood
- 89. Batch Delete “Delete all company.com accounts” DELETE /accounts? email=*@company.com @lhazlewood
- 90. Batch Create / Update Already have a CollecEon concept. Use it. @lhazlewood
- 91. Batch Create or Update POST /accounts { “items”: [ { ... account 1 ... }, { ... account 2 ... }, ... ] } @lhazlewood
- 92. Batch Operations: The ‘Catch’ Caching is bypassed enErely L @lhazlewood
- 93. 204 Accepted Location: /batches/a1b2c3 { “status”: “processing”, //overall status “size”: “n”, “limit”: 25, ..., “items”: { { response 1 (w/ individual status) ...}, { response 2 (w/ individual status) ...}, ... } } @lhazlewood
- 94. Errors @lhazlewood
- 95. • As descriptive as possible • As much information as possible • Developers are your customers @lhazlewood
- 96. POST /directories 409 Conflict { “status”: 409, “code”: 40924, “property”: “name”, “message”: “A Directory named ‘Avengers’ already exists.”, “developerMessage”: “A directory named ‘Avengers’ already exists. If you have a stale local cache, please expire it now.”, “moreInfo”: “https://www.stormpath.com/docs/ api/errors/40924” } @lhazlewood
- 97. Security @lhazlewood
- 98. Avoid sessions when possible Authenticate every request if necessary Stateless Authorize based on resource content, NOT URL! Use Existing Protocol: Oauth 1.0a, Oauth2, Basic over SSL only Custom Authentication Scheme: Only if you provide client code / SDK Only if you really, really know what you’re doing Use API Keys instead of Username/Passwords @lhazlewood
- 99. 401 vs 403 • 401 “Unauthorized” really means Unauthenticated “You need valid credentials for me to respond to this request” • 403 “Forbidden” really means Unauthorized “Sorry, you’re not allowed!” @lhazlewood
- 100. HTTP Authentication Schemes
• Server
response
to
issue
challenge:
WWW-Authenticate:
realm=“Application Name” • Client request to submit credenEals: Authorization: @lhazlewood - 101. API Keys • • • • • • • Entropy Password Reset Independence Scope Speed Limited Exposure Traceability @lhazlewood
- 102. IDs @lhazlewood
- 103. • IDs should be opaque • Should be globally unique • Avoid sequential numbers (contention, fusking) • Good candidates: UUIDs, ‘Url64’ @lhazlewood
- 104. HTTP Method Overrides @lhazlewood
- 105. POST /accounts/x7y8z9?_method=DELETE @lhazlewood
- 106. Caching & Concurrency Control @lhazlewood
- 107. Server (iniEal response): ETag: "686897696a7c876b7e” Client (later request): If-None-Match: "686897696a7c876b7e” Server (later response): 304 Not Modified @lhazlewood
- 108. Maintenance @lhazlewood
- 109. Use HTTP Redirects Create abstraction layer / endpoints when migrating Use well defined custom Media Types @lhazlewood
- 110. .com • • • • • • • Free for developers Eliminate months of development Automatic security best practices Single Sign On for your apps API Authentication & Key Management Token Authentication for SPAs / Mobile Authorization Libraries and integraEons: h`ps://docs.stormpath.com @lhazlewood