REST API design: POST vs GET DTO voor zelfde resource

Pagina: 1
Acties:

Vraag


Acties:
  • 0 Henk 'm!

  • gnoe93
  • Registratie: September 2016
  • Laatst online: 08-04 13:00
Ik heb een REST API waarbij ik eenzelfde resource zowel moet kunnen ophalen als aanmaken (GET en POST). De class/entity die deze resource omvat, heeft heel wat onderliggende relaties, welke voornamelijk overeenkomen met eenvoudige lookup tabellen (entities met enkel een "ID" en "name" property). Voor elk van deze lookup tabellen bestaat er ook een API endpoint/resource.

Ik vraag me nu af hoe ik dit best deze onderliggende relaties weergeef binnen de API. Stel volgend voorbeeld:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/api/products/24545
{
    id: 24545,
    name: "product A",
    tags: [
        "Tag A",
        "Tag B",
        "Tab C"
    ],
    category: "Category A"
}

/api/tags/9845
{
    id: 9845,
    name: "Tag A"
}

/api/categories/9845
{
    id: 9845,
    name: "Category A"
}


De product resource heeft een aantal tags en een enkele categorie. Omdat tags en categorieen aparte resources zijn, verwijs ik vanuit de product resource er enkel naar met hun naam. Hier heb ik een aantal bedenkingen bij.

Het is niet direct duidelijk voor de API client dat tags en category aparte resources zijn. Ik zou i.p.v. de naam de ID kunnen gebruiken (of er gewoon met de resource URL naar verwijzen, dan is het "echt" REST), maar dan moeten er extra calls gedaan worden om tags/categorieen op te halen (lijkt me verspilling voor simpelweg de naam).

Het tweede wat ik me afvraag is hoe ik het best producten aanmaak (POST). Stel nu dat ik zoals in mijn voorbeeld de naam gebruik voor tags en categorieen, gebruik ik dan best gewoonweg dezelfde json (DTO) voor het aanmaken van producten? Of vervang ik de naam dan best door de ID?

Kan iemand me misschien een voorbeeld van bestaande implementaties geven?

Alle reacties


Acties:
  • 0 Henk 'm!

  • P1nGu1n
  • Registratie: Juni 2011
  • Laatst online: 22:04
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/api/products/24545
{
    id: 24545,
    name: "product A",
    tags: [
        {
            id: 9845,
            name: "Tag B"
        },
        {
            id: 9846,
            name: "Tag B"
        },
        {
            id: 9847,
            name: "Tag C"
        }
    ],
    category: {
        id: 9845,
        name: "Category A"
    }
}

/api/tags/9845
{
    id: 9845,
    name: "Tag A"
}

/api/categories/9845
{
    id: 9845,
    name: "Category A"
}


Is dit een idee?

Vraag 2 begrijp ik niet helemaal :)

[ Voor 3% gewijzigd door P1nGu1n op 17-12-2017 17:47 ]

Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.


Acties:
  • 0 Henk 'm!

  • gnoe93
  • Registratie: September 2016
  • Laatst online: 08-04 13:00
P1nGu1n schreef op zondag 17 december 2017 @ 17:43:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/api/products/24545
{
    id: 24545,
    name: "product A",
    tags: [
        {
            id: 9845,
            name: "Tag B"
        },
        {
            id: 9846,
            name: "Tag B"
        },
        {
            id: 9847,
            name: "Tag C"
        }
    ],
    category: {
        id: 9845,
        name: "Category A"
    }
}

/api/tags/9845
{
    id: 9845,
    name: "Tag A"
}

/api/categories/9845
{
    id: 9845,
    name: "Category A"
}


Is dit een idee?
Dit is eigenlijk wat ik probeer te vermijden omwille van 2 redenen:

Ik heb hier nu als voorbeeld categories en tags gebruikt, maar in mijn datamodel zijn er een aantal lookup tabellen die naast ID en name nog een paar andere "metadata" kolommen hebben zoals "code/abbreviation", "imageUrl", etc. Ik wil daarom niet telkens het gehele object serializen als een ID/naam in principe genoeg is.

De andere reden is eigenlijk deel van mijn vraag: dit lijkt te doen voor het weergeven (serializen), maar wat doe je bij het posten van de data, aangezien het gehele object meegegeven overbodig is (enkel de ID is voldoende).

Acties:
  • 0 Henk 'm!

  • whoami
  • Registratie: December 2000
  • Laatst online: 22:56
gnoe93 schreef op zondag 17 december 2017 @ 17:17:
Ik heb een REST API waarbij ik eenzelfde resource zowel moet kunnen ophalen als aanmaken (GET en POST). De class/entity die deze resource omvat, heeft heel wat onderliggende relaties, welke voornamelijk overeenkomen met eenvoudige lookup tabellen (entities met enkel een "ID" en "name" property). Voor elk van deze lookup tabellen bestaat er ook een API endpoint/resource.

Ik vraag me nu af hoe ik dit best deze onderliggende relaties weergeef binnen de API. Stel volgend voorbeeld:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/api/products/24545
{
    id: 24545,
    name: "product A",
    tags: [
        "Tag A",
        "Tag B",
        "Tab C"
    ],
    category: "Category A"
}

/api/tags/9845
{
    id: 9845,
    name: "Tag A"
}

/api/categories/9845
{
    id: 9845,
    name: "Category A"
}


De product resource heeft een aantal tags en een enkele categorie. Omdat tags en categorieen aparte resources zijn, verwijs ik vanuit de product resource er enkel naar met hun naam. Hier heb ik een aantal bedenkingen bij.

Het is niet direct duidelijk voor de API client dat tags en category aparte resources zijn. Ik zou i.p.v. de naam de ID kunnen gebruiken (of er gewoon met de resource URL naar verwijzen, dan is het "echt" REST), maar dan moeten er extra calls gedaan worden om tags/categorieen op te halen (lijkt me verspilling voor simpelweg de naam).
Is de Id van een categorie of Tag belangrijk voor de client / consumer ? Indien niet, dan moet je het niet meesturen.
Het tweede wat ik me afvraag is hoe ik het best producten aanmaak (POST). Stel nu dat ik zoals in mijn voorbeeld de naam gebruik voor tags en categorieen, gebruik ik dan best gewoonweg dezelfde json (DTO) voor het aanmaken van producten? Of vervang ik de naam dan best door de ID?
Ik zou gewoon hetzelfde model gebruiken. Je moet er dan natuurlijk wel in je implementatie gaan checken welke tag / categorie er moet gebruikt worden.

Hoe kan de consumer bij het aanmaken van een dergelijk object de tags of categorieën gaan toewijzen ? Vrije tekst, of is dat te selecteren uit een lijstje ?

Je kan er natuurlijk ook voor kiezen om zowel id als omschrijving in je model te stoppen:

code:
1
2
3
4
5
6
7
8
9
{
    id: 24545,
    name: "product A",
    tags: [
        { "id": "1234", "descr":  "Tag A" },
        { "id": "567", "descr": "Tag B" }
    ],
    category: { "id": "1", "descr": "Category A" }
}

https://fgheysels.github.io/


Acties:
  • 0 Henk 'm!

  • gnoe93
  • Registratie: September 2016
  • Laatst online: 08-04 13:00
whoami schreef op zondag 17 december 2017 @ 17:55:
[...]
Is de Id van een categorie of Tag belangrijk voor de client / consumer ? Indien niet, dan moet je het niet meesturen.


[...]

Ik zou gewoon hetzelfde model gebruiken. Je moet er dan natuurlijk wel in je implementatie gaan checken welke tag / categorie er moet gebruikt worden.

Hoe kan de consumer bij het aanmaken van een dergelijk object de tags of categorieën gaan toewijzen ? Vrije tekst, of is dat te selecteren uit een lijstje ?

Je kan er natuurlijk ook voor kiezen om zowel id als omschrijving in je model te stoppen:

code:
1
2
3
4
5
6
7
8
9
{
    id: 24545,
    name: "product A",
    tags: [
        { "id": "1234", "descr":  "Tag A" },
        { "id": "567", "descr": "Tag B" }
    ],
    category: { "id": "1", "descr": "Category A" }
}
Het is eerder een publieke API waar clients mee kunnen doen wat ze willen, er is niet direct een front end aan verbonden. Er is dus geen dropdown of tekstveld om categorieen uit te selecteren.

Ik denk niet dat het voor een client belangrijk is ook de ID te hebben naast de naam (als hij de naam heeft, dan heeft hij de ID niet meer nodig om de naam op te halen). Maar omdat het een publieke API is, lijkt het me duidelijker toch de ID mee te geven om kenbaar te maken dat het weldegelijk om een aparte resource gaat die ook opgevraagd kan worden met de ID.

De meest beknopte manier om dit te bereiken, lijkt inderdaad wat je net voorstelde (beiden ID en name (description)).

Hoe zou je met deze manier POST requests verwerken? Enkel de ID meesturen binnen {} om het consistent te houden?

code:
1
2
3
4
5
6
7
8
9
{
    id: 24545,
    name: "product A",
    tags: [
        { "id": "1234" },
        { "id": "567" }
    ],
    category: { "id": 1 }
}

Acties:
  • +4 Henk 'm!

  • drm
  • Registratie: Februari 2001
  • Laatst online: 09-06 13:31

drm

f0pc0dert

Het is m.i. het meest clean als de structuur van je POST/PUT hetzelfde is als de structuur van je GET. M.a.w. als je een PUT doet met de inhoud van de GET verandert er niets. Desondanks is er best wat voor te zeggen om in je GET keuzes te hebben in hoe je de relaties zou willen ophalen. Omwille van efficientie is het denkbaar dat een GET inclusief namen handig is, maar het is net zo goed denkbaar dat het omwille van efficientie juist handiger is om de client zelf de lookups te laten doen (denk bijvoorbeeld aan een SPA waarbij alle beschikbare relaties op meerdere plekken gerenderd moeten worden).

Ik zou daarom waarschijnlijk kiezen voor een design waar beide mogelijk is. Bijvoorbeeld zoiets:

GET /products/123
code:
1
2
3
4
5
{
   "id": 123,
   "name": "abc",
   "tags": [1, 2, 3]
}


GET /products/123?expand=tags
code:
1
2
3
4
5
6
7
8
9
{
    "id": 123,
    "name": "abc",
    "tags": [
       {"id": 1, "name": "one"}, 
       {"id": 2, "name": "two"}, 
       {"id": 3, "name": "three"}
    ]
}


GET /tags/1
code:
1
2
3
4
{
    "id": 1,
    "name": "one"
}


GET /tags?id=1&id=2&id=3
code:
1
2
3
4
5
[
   {"id": 1, "name": "one"}, 
   {"id": 2, "name": "two"}, 
   {"id": 3, "name": "three"}
]


Verder is er best wat voor te zeggen om de client controle te geven over welke velden hij wil zien. Meestal weet de client immers al weet hij nodig heeft, de service niet.

Bijvoorbeeld:

GET /product/123?field=tags
code:
1
2
3
{
    "tags": [1, 2, 3]
}


Of zelfs:
GET /product/123?field=tags&expand=tags&field=tags.name
code:
1
2
3
{
    "tags": [{"name": "one"}, {"name": "two"}, {"name": "three"}]
}


Als je dit in je (de)serializatielaag netjes oplost is het vrij eenvoudig om dat generiek te bouwen en hou je heel veel oplossingsrichtingen voor de client open.

Ook het ondersteunen van zowel object- als ID referenties in POST/PUT/PATCH is prima generiek te implementeren, en zou volgens het adagium dat een PUT op een resource met de inhoud die je van een GET gekregen hebt geen wijzigingen op zou mogen leveren, zelfs een min of meer morele verplichting zijn ;) Het resultaat is dat je dezelfde structuur hanteert voor PUT/POST/PATCH waarbij je de client verplicht aan te geven wat hij zelf wil doen:

PUT /product/123?field=tags
code:
1
2
3
{
    "tags": [1, 2, 3]
}


PUT /product/123?field=tags&expand=tags
code:
1
2
3
{
    "tags": [{"id": 1}, {"id": 2}, {"id": 3}]
}


Tot slot zou je kunnen overwegen om het HTTP caching model met E-tags te implementeren om versieconflicten te voorkomen. Dan hou je het echt helemaal clean en consistent.

Music is the pleasure the human mind experiences from counting without being aware that it is counting
~ Gottfried Leibniz


Acties:
  • 0 Henk 'm!

  • gnoe93
  • Registratie: September 2016
  • Laatst online: 08-04 13:00
drm schreef op zondag 17 december 2017 @ 20:33:
Het is m.i. het meest clean als de structuur van je POST/PUT hetzelfde is als de structuur van je GET. M.a.w. als je een PUT doet met de inhoud van de GET verandert er niets. Desondanks is er best wat voor te zeggen om in je GET keuzes te hebben in hoe je de relaties zou willen ophalen. Omwille van efficientie is het denkbaar dat een GET inclusief namen handig is, maar het is net zo goed denkbaar dat het omwille van efficientie juist handiger is om de client zelf de lookups te laten doen (denk bijvoorbeeld aan een SPA waarbij alle beschikbare relaties op meerdere plekken gerenderd moeten worden).

Ik zou daarom waarschijnlijk kiezen voor een design waar beide mogelijk is. Bijvoorbeeld zoiets:

GET /products/123
code:
1
2
3
4
5
{
   "id": 123,
   "name": "abc",
   "tags": [1, 2, 3]
}


GET /products/123?expand=tags
code:
1
2
3
4
5
6
7
8
9
{
    "id": 123,
    "name": "abc",
    "tags": [
       {"id": 1, "name": "one"}, 
       {"id": 2, "name": "two"}, 
       {"id": 3, "name": "three"}
    ]
}


GET /tags/1
code:
1
2
3
4
{
    "id": 1,
    "name": "one"
}


GET /tags?id=1&id=2&id=3
code:
1
2
3
4
5
[
   {"id": 1, "name": "one"}, 
   {"id": 2, "name": "two"}, 
   {"id": 3, "name": "three"}
]


Verder is er best wat voor te zeggen om de client controle te geven over welke velden hij wil zien. Meestal weet de client immers al weet hij nodig heeft, de service niet.

Bijvoorbeeld:

GET /product/123?field=tags
code:
1
2
3
{
    "tags": [1, 2, 3]
}


Of zelfs:
GET /product/123?field=tags&expand=tags&field=tags.name
code:
1
2
3
{
    "tags": [{"name": "one"}, {"name": "two"}, {"name": "three"}]
}


Als je dit in je (de)serializatielaag netjes oplost is het vrij eenvoudig om dat generiek te bouwen en hou je heel veel oplossingsrichtingen voor de client open.

Ook het ondersteunen van zowel object- als ID referenties in POST/PUT/PATCH is prima generiek te implementeren, en zou volgens het adagium dat een PUT op een resource met de inhoud die je van een GET gekregen hebt geen wijzigingen op zou mogen leveren, zelfs een min of meer morele verplichting zijn ;) Het resultaat is dat je dezelfde structuur hanteert voor PUT/POST/PATCH waarbij je de client verplicht aan te geven wat hij zelf wil doen:

PUT /product/123?field=tags
code:
1
2
3
{
    "tags": [1, 2, 3]
}


PUT /product/123?field=tags&expand=tags
code:
1
2
3
{
    "tags": [{"id": 1}, {"id": 2}, {"id": 3}]
}


Tot slot zou je kunnen overwegen om het HTTP caching model met E-tags te implementeren om versieconflicten te voorkomen. Dan hou je het echt helemaal clean en consistent.
Bedankt voor je uitgebreide reactie :). Juist nog een paar vragen/opmerkingen.

Is het misschien niet beter om bij de niet-expanded versie de IDs toch in {} te zetten voor consistentie? Anders moeten de clients mogelijks aparte logica voorzien om het resultaat te mappen.

code:
1
2
3
{
    "tags": [{"id": 1}, {"id": 2}, {"id": 3}]
}


In plaats van:

code:
1
2
3
{
    "tags": [1, 2, 3]
}


Een andere vraag is hoe je ?expanded=tags zou afhandelen indien een tag meerdere geneste relaties heeft. Misschien wil de gebruiker enkel de tags zelf expanden maar niet bijvoorbeeld de "tag category".

[ Voor 3% gewijzigd door gnoe93 op 18-12-2017 05:32 ]


Acties:
  • +1 Henk 'm!

  • Hydra
  • Registratie: September 2000
  • Laatst online: 06-10 13:59
@gnoe93 er is hier een soort van standaard voor in REST-land; HAL. Nadeel is dat de implementatie ervan veel (omslachtig) hand-werk is en de responses voor de client niet erg simpel te parsen zijn. Als het een enkele monolitische API is zou ik dan eerder voor GraphQL gaan als je het de gebruiker zelf wil laten doen.

In onze API voegen we de 'dingen' in waarvan we denken dat de client ze waarschijnlijk in diezelfde request goed kan gebruiken. De andere dingen verwijzen we naar via HATEOAS links
drm schreef op zondag 17 december 2017 @ 20:33:
Ik zou daarom waarschijnlijk kiezen voor een design waar beide mogelijk is. Bijvoorbeeld zoiets:

GET /products/123
code:
1
2
3
4
5
{
   "id": 123,
   "name": "abc",
   "tags": [1, 2, 3]
}


GET /products/123?expand=tags
code:
1
2
3
4
5
6
7
8
9
{
    "id": 123,
    "name": "abc",
    "tags": [
       {"id": 1, "name": "one"}, 
       {"id": 2, "name": "two"}, 
       {"id": 3, "name": "three"}
    ]
}
Je bent nu eigenlijk http://graphql.org/ opnieuw aan 't uitvinden. Je bent een hoop logica aan het inbouwen waarmee je de code complexer maakt. Al die 'expand' logica moet je wel ff bouwen en onderhouden en versioneren.

https://niels.nu


Acties:
  • 0 Henk 'm!

  • drm
  • Registratie: Februari 2001
  • Laatst online: 09-06 13:31

drm

f0pc0dert

Hydra:
Je bent nu eigenlijk http://graphql.org/ opnieuw aan 't uitvinden.
Het heeft er wat van weg, ja.
Je bent een hoop logica aan het inbouwen waarmee je de code complexer maakt. Al die 'expand' logica moet je wel ff bouwen en onderhouden en versioneren.
Ik wil maar aangeven dat de beslissing of iets wel of niet meegenomen moet worden beter goed uitgedacht kan worden alvorens je je api in beton giet. De beslissing bij de gebruiker leggen is dan wellicht ook het overwegen waard.

Maar je hebt gelijk, het toevoegen van een compleet nieuwe domeintaal en abstractielaag is veel eenvoudiger. ;)


gnoe93:
Is het misschien niet beter om bij de niet-expanded versie de IDs toch in {} te zetten voor consistentie? Anders moeten de clients mogelijks aparte logica voorzien om het resultaat te mappen.
"beter" of niet kan ik niet voor je beantwoorden. Maar het is wat mij betreft zeker een terechte overweging.
Een andere vraag is hoe je ?expanded=tags zou afhandelen indien een tag meerdere geneste relaties heeft. Misschien wil de gebruiker enkel de tags zelf expanden maar niet bijvoorbeeld de "tag category".
Ja ik zou daar altijd kiezen voor expliciet en consistent. Dus "tags" slaat op het veld van je root entity. Om de categorie ook weer te expanden zou je dan bijvoorbeeld `tags.category` ook expliciet mee moeten geven.

Maar ik zou het desondanks wel pragmatisch houden. Voor je het weet heb je inderdaad graphql nagemaakt.

[ Voor 40% gewijzigd door drm op 19-12-2017 18:39 ]

Music is the pleasure the human mind experiences from counting without being aware that it is counting
~ Gottfried Leibniz

Pagina: 1