Upgrade to Pro — share decks privately, control downloads, hide ads and more …

mauvaises bonnes idées pour REST

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.

xordoquy

September 24, 2017
Tweet

More Decks by xordoquy

Other Decks in Programming

Transcript

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

    Django REST framework / django-statsd • freelance (depuis 2004) Django API coaching
  2. • 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)
  3. Contraintes • Architecture client-serveur • Sans états • Usage de

    caches • Système en couches • Interface uniforme
  4. 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)
  5. 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/', ... ] }
  6. Application A Application B LDAP / AD Application C Application

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

    C Application C Application C WebService permissions Liste noire
  8. GET /users/ ["xordoquy", "janedoe", "johndae"] GET /users/xordoquy/ { "admin": true,

    "projects": { "dango-rest-framework": ["owner"], "whatever": ["developer"] } }
  9. GET /users/ [{"name": "xordoquy"}, {"name": "janedoe"}] GET /users/xordoquy/ { "name":

    "xordoquy", "admin": true, "projects": [{ "name": "dango-rest-framework", "groups": ["owner"] }, { "name": "whatever", "groups": ["developer"] }] }
  10. GET /user/xordoquy/ {"projects": [{ "name": "drf", "groups": ["owner"] }]} POST

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

    /user/xordoquy/ {"projects": [{ "name": "drf", "groups": ["owner"] }]} Concurrence:
  12. 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:
  13. { "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" }] }] }
  14. { "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
  15. • Optimisation prématurée ? • En manque de GraphQL ?

    • Simplement trop lié au client ?
  16. 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, ...], }
  17. 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}, ... ], }
  18. 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}, ... ], }
  19. 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" }]
  20. GET /users/xordoquy/permissions/ [{ "id": 1, "name": "owner", "project": "dango-rest-framework", "user":

    "xordoquy" }, { "id": 2, "name": "developer", "project": "whatever", "user": "xordoquy" }]
  21. 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" }]
  22. 404

  23. • 403: Unauthorized • 404: Not Found • 409: Conflict

    Ne s’appliquent pas au ressources associées:
  24. "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" } ] },
  25. 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')
  26. Compatible • Nouvelle ressource • Nouveau paramètre optionnel pour url

    • Nouvelle donnée optionnelle pour la création / modification • Nouvelle donnée pour la lecture
  27. 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)
  28. 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/" ] }
  29. <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
  30. 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
  31. // 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}) }) } }
  32. // 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 } }
  33. // getters.js export default { list: state => (type) =>

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

    • Full HATEOAS (sans aucune url coté client)