mauvaises bonnes idées pour REST

F986d9d2a1920cd4c9c71110a2dc7905?s=47 xordoquy
September 24, 2017

mauvaises bonnes idées pour REST

Pycon.fr 2017 - retour d'expérience sur des idées qui paraissaient bonnes et sont révélées problématiques.

F986d9d2a1920cd4c9c71110a2dc7905?s=128

xordoquy

September 24, 2017
Tweet

Transcript

  1. fausses bonnes idées avec REST Xavier Ordoquy Pycon fr 2017

  2. • Xavier Ordoquy • linovia / @linovia_net • core dev:

    Django REST framework / django-statsd • freelance (depuis 2004) Django API coaching
  3. None
  4. • 7 membres, 708 contributeurs, • 7460 commits, 98 releases,

    • 2826 (35) pull requests, 2617 (136) issues, • 4 versions de Python / 4 versions de Django • matrice de 16 environnements dont 2 de qualité • Build travis complet de 60 à 100 secondes / env • tests: 1099 (~ 15 secondes) • Tox: ~ 21s (paquets installés) / ~ 45s (env vierge)
  5. Retours d’expériences (bonnes ou mauvaises) bienvenues

  6. Retour d’expérience

  7. None
  8. None
  9. Rappels

  10. REST • REpresentational State Transfer • Roy Fielding • Contraintes

    d’architecture
  11. Contraintes • Architecture client-serveur • Sans états • Usage de

    caches • Système en couches • Interface uniforme
  12. Contraintes • Interface uniforme: • Identification des ressources dans les

    requêtes • Manipulation des ressources par représentations • Messages auto-décrits • Hypermedia comme moteur de l’état de l’application (HATEOAS)
  13. Vocabulaire Point d’entrée /api/ Collection /api/users/ Resource /api/users/5/ Représentation {

    'album_name': 'Graceland', 'artist': 'Paul Simon', 'tracks': [ 'http://test.com/api/tracks/45/', 'http://test.com/api/tracks/46/', 'http://test.com/api/tracks/47/', ... ] }
  14. Fil conducteur: première API Gestion centralisée de permissions

  15. None
  16. Application A Application B LDAP / AD Application C Application

    C Application C Application C
  17. Application A Application B LDAP / AD Application C Application

    C Application C Application C Liste noire
  18. Application A Application B LDAP / AD Application C Application

    C Application C Application C WebService permissions Liste noire
  19. GET /projects/ ["django-rest-framework", "whatever"] GET /projects/demo/ { "groups": { "owner":

    ["xordoquy"], "developer": ["janedoe"], "reviewer": [] } }
  20. GET /users/ ["xordoquy", "janedoe", "johndae"] GET /users/xordoquy/ { "admin": true,

    "projects": { "dango-rest-framework": ["owner"], "whatever": ["developer"] } }
  21. Problème: Impossible d’ajouter des attributs

  22. GET /users/ [{"name": "xordoquy"}, {"name": "janedoe"}] GET /users/xordoquy/ { "name":

    "xordoquy", "admin": true, "projects": [{ "name": "dango-rest-framework", "groups": ["owner"] }, { "name": "whatever", "groups": ["developer"] }] }
  23. Problème: Complexité en écriture

  24. GET /user/xordoquy/ {"projects": [{ "name": "drf", "groups": ["owner"] }]} Concurrence:

  25. GET /user/xordoquy/ {"projects": [{ "name": "drf", "groups": ["owner"] }]} POST

    /user/xordoquy/ {"projects": [{ "name": "drf", "groups": [ "owner", "developer" ] }]} Concurrence:
  26. POST /user/xordoquy/ {"projects": [{ "name": "drf", "groups": [] }]} GET

    /user/xordoquy/ {"projects": [{ "name": "drf", "groups": ["owner"] }]} Concurrence:
  27. POST /user/xordoquy/ {"projects": [{ "name": "drf", "groups": [] }]} GET

    /user/xordoquy/ {"projects": [{ "name": "drf", "groups": ["owner"] }]} POST /user/xordoquy/ {"projects": [{ "name": "drf", "groups": [ "owner", "developer" ] }]} Concurrence:
  28. None
  29. { "name": "Project demo", "lines": [{ "name": "TODO", "cards": [{

    "label": "Preparer ses slides", "owner": "xordoquy" }] }, { "name": "Ongoing", "cards": [{ "label": "Pycon.fr" "owner": "plein de monde" }] }, { "name": "Done", "cards": [{ "label": "Annoncer la DjangCong !!" "owner": "DjangoFR" }] }] }
  30. { "name": "Ride the lightning", "tracks": [{ "name": "For Whom

    the Bell Tolls", "vocals": [{ "name": "James Hetfield", "name": "Lars Ulrich", "born": "December 26, 1963" }] }, { "name": "Fight Fire with Fire", "vocals": [{ "name": "James Hetfield", "born": "December 26, 1963" }] }] } Pire
  31. • Optimisation prématurée ? • En manque de GraphQL ?

    • Simplement trop lié au client ?
  32. Garder de la cohérence

  33. GET /album/56/ { 'album_name': 'The Grey Album', 'artist': 'Danger Mouse',

    'tracks': [ {'id': 1, 'title': 'Public Announcement', 'duration': 245}, {'id': 2, 'title': 'What More Can I Say', 'duration': 264}, ... ], } POST /album/56/ { 'album_name': 'The Grey Album', 'artist': 'Danger Mouse', 'tracks': [1, 2, 3, ...], }
  34. GET /album/56/ { 'album_name': 'The Grey Album', 'artist': 'Danger Mouse',

    'tracks': [ {'id': 1, 'title': 'Public Announcement', 'duration': 245}, {'id': 2, 'title': 'What More Can I Say', 'duration': 264}, ... ], } POST /album/56/ { 'album_name': 'The Grey Album', 'artist': 'Danger Mouse', 'tracks': [ {'id': 1}, {'id': 2}, ... ], }
  35. POST /album/56/ { 'album_name': 'The Grey Album', 'artist': 'Danger Mouse',

    'track_ids': [1, 2, 3, ...], } GET /album/56/ { 'album_name': 'The Grey Album', 'artist': 'Danger Mouse', 'track_ids': [1, 2, 3, ...], 'tracks': [ {'id': 1, 'title': 'Public Announcement', 'duration': 245}, {'id': 2, 'title': 'What More Can I Say', 'duration': 264}, ... ], }
  36. Besoin: Gérer les permissions

  37. Ressource: permissions

  38. GET /permissions/ [{ "id": 1, "name": "owner", "project": "dango-rest-framework", "user":

    "xordoquy" }, { "id": 2, "name": "developer", "project": "whatever", "user": "xordoquy" }, { "id": 3, "name": "developer", "project": "whatever", "user": "janedoe" }]
  39. Permissions par utilisateurs ou projet ?

  40. GET /users/xordoquy/permissions/ [{ "id": 1, "name": "owner", "project": "dango-rest-framework", "user":

    "xordoquy" }, { "id": 2, "name": "developer", "project": "whatever", "user": "xordoquy" }]
  41. GET /users/xordoquy/permissions/ GET /users/xordoquy/permissions/owner/ GET /users/xordoquy/permissions/owner/project/drf GET /users/xordoquy/project/drf GET /project/drf

    GET /project/drf/users/xordoquy/ GET /project/drf/permissions/owner/
  42. GET /permissions/1/ GET /users/xordoquy/permissions/1/ GET /users/xordoquy/project/drf/permissions/1/ GET /project/drf/permissions/1/ GET /project/drf/users/xordoquy/permissions/1/

    Identification de la ressource
  43. GET /users/xordoquy/permissions/owner/project/drf 404 ou 403 pour: -xordoquy ? -owner ?

    -drf ? -un peu de tout ? Codes d’erreurs
  44. -Heroku - http api design guide Minimize path nesting

  45. /orgs/{org_id}/apps/{app_id}/dynos/ {dyno_id} vs /orgs/{org_id} /orgs/{org_id}/apps /apps/{app_id} /apps/{app_id}/dynos /dynos/{dyno_id}

  46. Collections + filtres = ensembles de données + contraintes

  47. /permissions/ permissions

  48. /permissions/ xordoquy /permissions/?owner=xordoquy permissions

  49. /permissions/ /permissions/?project=drf permissions DRF

  50. /permissions/ les miennes /permissions/?owner=xordoquy /permissions/?project=drf /permissions/?owner=xordoquy&project=drf permissions DRF

  51. Pourtant…

  52. GET /repos/:owner/:repo/pulls/:number https://github.com/repos/encode/apistar/pull/5443 Github

  53. Nécessaire avec • segmentation (github) • clefs composites (et encore)

  54. Revenons à notre API

  55. GET /permissions/ [{ "id": 1, "name": "owner", "project": "dango-rest-framework", "user":

    "xordoquy" }, { "id": 2, "name": "developer", "project": "whatever", "user": "xordoquy" }, { "id": 3, "name": "developer", "project": "whatever", "user": "janedoe" }]
  56. POST /permissions/ { "name": "watcher", "project": "dango-rest-framework", "user": "xordoquy" }

    Créons une permission
  57. 404

  58. POST /permissions/ { "name": "watcher", << NOT FOUND "project": "dango-rest-framework",

    "user": "xordoquy" } Créons une permission
  59. POST /permissions/ { "name": "watcher", "project": "dango-rest-framework", "user": "xordoquy" }

    400 Bad request { "name": ["unknown permission"] }
  60. • 403: Unauthorized • 404: Not Found • 409: Conflict

    Ne s’appliquent pas au ressources associées:
  61. Relations entre représentations

  62. Requête OPTIONS

  63. "compagnies": { "type": "field", "label": "client", "required": true, "read_only": false,

    "choices": [ { "value": "/api/compagnies/1/", "display_name": "Linovia" }, { "value": "/api/compagnies/2/", "display_name": "Some nice corp" }, ... { "value": "/api/compagnies/4/", "display_name": "ACME" } ] },
  64. Génération automatique de formulaires mais…

  65. None
  66. "compagnies": { "type": "field", "label": "client", "required": true, "read_only": false

    },
  67. Cache les symptômes

  68. Mieux: Limiter l’ensemble associé

  69. class ClientPKField(serializers.PrimaryKeyRelatedField): def get_queryset(self): user = self.context['request'].user queryset = Client.objects.filter(user=user)

    return queryset class InvoiceSerializer(serializers.ModelSerializer): clients = ClientPKField(many=True) class Meta: model = Invoice fields = ('id', 'name', 'clients')
  70. Compatibilité ascendante descendante

  71. Compatible • Nouvelle ressource • Nouveau paramètre optionnel pour url

    • Nouvelle donnée optionnelle pour la création / modification • Nouvelle donnée pour la lecture
  72. Incompatible • Paramètre d’url obligatoire • Donnée obligatoire pour la

    création / modification • Suppression d’une ressource • Suppression d’un verbe HTTP pour une ressource • Changement de comportement d’une ressource (valeurs par défaut)
  73. Ne marche qu’avec un parsing « relaxé »

  74. projet pure REST: DRF + VueJS

  75. Ligne Facture Emetteur Client Ligne Ligne Ligne Lignes

  76. None
  77. None
  78. HTTP 200 OK Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS

    Content-Type: application/json Vary: Accept { "id": "3ffa61af", "url": "http://localhost:8000/api/invoices/3ffa61af/", "name": "Test invoice", "additional_infos": "Lorem ipsum [...] commodo augue.", "owner": "http://localhost:8000/api/users/1/", "client": "http://localhost:8000/api/companies/5a50d85f/", "creation_date": "2017-06-14T12:35:49.385446Z", "update_date": "2017-09-23T14:37:57.776931Z", "items": [ "http://localhost:8000/api/invoice_items/96210bfd/", "http://localhost:8000/api/invoice_items/ee50e3f0/" ] }
  79. <div> ... <v-row> <v-card> <user :url="invoice.owner"/> </v-card> <v-card> <company :url="invoice.client"/>

    </v-card> </v-row> <v-row> <v-card> <v-table :items="invoice.items"> <template slot="items" scope="props"> <item :url="props.item"/> </template> </v-table> </v-card> </v-row> </div> Invoice
  80. export default { name: 'company', props: { url: String, },

    created () { this.fetchData() }, methods: { fetchData (args) { this.$store.dispatch('get_detail', { uri: this.$props.url, type: 'companies' }) } }, computed: { company () { return this.$store.getters.client(this.$props.url) } } } Company
  81. Vuex Comp Actions dispatch Mutations commit State getters

  82. // actions.js export default { get_detail ({commit, state, dispatch}, {uri,

    type}) { axios(uri) .then(function (response) { commit('new_content', {type: type, data: response.data}) }) }, get_list ({commit, state, dispatch}, {type}) { axios(state['urls'][type]) .then(function (response) { commit('new_contents', {type: type, data: response.data}) }) } }
  83. // mutations.js export default { new_content (state, {type, data}) {

    state[type] = state[type].filter( item => item.url !== data.url) state[type] = state[type].concat([data]) }, new_contents (state, {type, data}) { state[type] = data } }
  84. // getters.js export default { list: state => (type) =>

    { return state[type] }, detail: state => (type, url) => { return state[type].find(object => object.url === url) } }
  85. A étudier • URL Templates (RFC 6570 - /search{?q,lang} )

    • Full HATEOAS (sans aucune url coté client)
  86. Questions ?