Pojęcia dotyczące interfejsu API REST

10

Mam trzy pytania dotyczące projektu interfejsu API REST, które mam nadzieję, że ktoś może rzucić nieco światła. Szukałem bez końca od wielu godzin, ale nigdzie nie znalazłem odpowiedzi na moje pytania (może po prostu nie wiem, czego szukać?).

Pytanie 1

Moje pierwsze pytanie dotyczy działań / RPC. Od jakiegoś czasu opracowuję interfejs API REST i przywykłem do myślenia o kolekcjach i zasobach. Jednak natknąłem się na kilka przypadków, w których paradygmat wydaje się nie mieć zastosowania i zastanawiam się, czy istnieje sposób na pogodzenie tego z paradygmatem REST.

W szczególności mam przypadek, w którym modyfikacja zasobu powoduje wygenerowanie wiadomości e-mail. Jednak w późniejszym momencie użytkownik może wyraźnie wskazać, że chce ponownie wysłać wiadomość e-mail wysłaną wcześniej. Podczas ponownego wysyłania wiadomości e-mail żaden zasób nie jest modyfikowany. Żaden stan nie jest zmieniany. To po prostu działanie, które musi nastąpić. Działanie jest powiązane z określonym typem zasobu.

Czy właściwe jest mieszanie jakiegoś wywołania akcji z identyfikatorem URI zasobu (np. /collection/123?action=resendEmail)? Czy lepiej byłoby określić akcję i przekazać identyfikator zasobu (np. /collection/resendEmail?id=123)? Czy to niewłaściwy sposób na to? Tradycyjnie (przynajmniej z HTTP) wykonywana akcja jest metodą żądania (GET, POST, PUT, DELETE), ale tak naprawdę nie pozwalają na niestandardowe akcje z zasobem.

pytanie 2

Używam kwerendującego fragmentu adresu URL do filtrowania zestawu zasobów zwróconych podczas zapytania do kolekcji (np /collection?someField=someval.). Następnie w moim kontrolerze API określam, jakie porównanie będzie miało miejsce z tym polem i wartością. Odkryłem, że to naprawdę nie działa. Potrzebuję sposobu, aby pozwolić użytkownikowi interfejsu API określić typ porównania, które chce wykonać.

Najlepszym pomysłem, jaki do tej pory wymyśliłem, jest zezwolenie użytkownikowi interfejsu API na określenie go jako dodatku do nazwy pola (np. /collection?someField:gte=someval- aby wskazać, że powinien zwracać zasoby tam, gdzie someFieldjest większe lub równe (> =) cokolwiek somevaljest Czy to dobry pomysł? Zły pomysł? Jeśli tak, to dlaczego? Czy istnieje lepszy sposób, aby umożliwić użytkownikowi określenie rodzaju porównania do wykonania z danym polem i wartością?

pytanie 3

Często widzę URI, które wyglądają jak /person/123/dogszdobycie persons dogs. Generalnie unikałem czegoś takiego, ponieważ na koniec sądzę, że tworząc taki identyfikator URI, uzyskujesz dostęp do dogskolekcji filtrowanej według określonego personidentyfikatora. Byłoby to równoważne z /dogs?person=123. Czy naprawdę istnieje dobry powód, aby identyfikator URI REST miał głębokość większą niż dwa poziomy ( /collection/resource_id)?

Justin Warkentin
źródło
10
Masz trzy pytania. Dlaczego nie opublikować ich osobno?
anaximander
3
Lepiej byłoby podzielić to na 3 oddzielne pytania. Widz może być w stanie udzielić doskonałej odpowiedzi na jedno, ale nie na wszystkie pytania.
2
Myślę, że wszystkie są ze sobą powiązane. Tytuł jest trochę na wysokim poziomie, ale to pytanie pomoże wielu osobom i można je łatwo znaleźć podczas wyszukiwania SE. To pytanie powinno stać się Wiki społeczności po dodaniu wystarczającej liczby głosów i dodaniu treści. Zajęło mi tygodnie, aby zbadać te rzeczy.
Andrew T Finnell,
1
Być może lepiej byłoby opublikować je osobno, IDK. Jednak, jak wspomniał @AndrewFinnell, pomyślałem, że dobrze byłoby zachować pytania razem, ponieważ były to najtrudniejsze pytania związane z REST, jakie miałem i fajnie byłoby, gdyby inni ludzie mogli znaleźć odpowiedzi razem.
Justin Warkentin,

Odpowiedzi:

11

Czy właściwe jest mieszanie jakiegoś wywołania akcji z identyfikatorem URI zasobu (np. /collection/123?action=resendEmail)? Czy lepiej byłoby określić akcję i przekazać identyfikator zasobu (np. /collection/resendEmail?id=123)? Czy to niewłaściwy sposób na to? Tradycyjnie (przynajmniej z HTTP) wykonywana akcja jest metodą żądania (GET, POST, PUT, DELETE), ale tak naprawdę nie pozwalają na niestandardowe akcje z zasobem.

Wolałbym modelować to w inny sposób, z kolekcją zasobów reprezentujących wiadomości e-mail, które mają zostać wysłane; wysyłanie zostanie przetworzone przez wewnętrzny serwis w odpowiednim czasie, w którym to momencie odpowiedni zasób zostanie usunięty. (Lub użytkownik może wcześniej usunąć zasób, powodując anulowanie żądania wysłania.)

Cokolwiek robisz, nie umieszczaj czasowników w nazwie zasobu! To rzeczownik (a część zapytania to zestaw przymiotników). Nouning czasowniki dziwne REST!

Używam kwerendującego fragmentu adresu URL do filtrowania zestawu zasobów zwróconych podczas zapytania do kolekcji (np /collection?someField=someval.). Następnie w moim kontrolerze API określam, jakie porównanie będzie miało miejsce z tym polem i wartością. Odkryłem, że to naprawdę nie działa. Potrzebuję sposobu, aby pozwolić użytkownikowi interfejsu API określić typ porównania, które chce wykonać.

Najlepszym pomysłem, jaki do tej pory wymyśliłem, jest zezwolenie użytkownikowi interfejsu API na określenie go jako dodatku do nazwy pola (np. /collection?someField:gte=someval- aby wskazać, że powinien on zwracać zasoby, w których pole niektóre jest większe lub równe ( >=) cokolwiek somevaljest. Czy to dobry pomysł? Zły pomysł? Jeśli tak, to dlaczego? Czy istnieje lepszy sposób, aby umożliwić użytkownikowi określenie rodzaju porównania do wykonania z danym polem i wartością?

Wolę określić ogólną klauzulę filtrowania i mieć ją jako opcjonalny parametr zapytania przy każdym żądaniu pobrania zawartości kolekcji. Klient może następnie dokładnie określić, w jaki sposób ograniczyć zestaw zwracany, w dowolny sposób. Martwię się również trochę o możliwość wykrycia języka filtrów / zapytań; im bogatszy, tym trudniejszy do odkrycia dla dowolnych klientów. Alternatywnym podejściem, które, przynajmniej teoretycznie, zajmuje się tym zagadnieniem wykrywalności, jest umożliwienie tworzenia podrzędnych zasobów kolekcji, które klienci uzyskują przez POST dokumentu opisującego ograniczenie zasobu kolekcji. Nadal jest to niewielkie nadużycie, ale przynajmniej można je wyraźnie odkryć!

Tego rodzaju wykrywalność jest jedną z rzeczy, które uważam za najmniej silne przy REST.

Często widzę URI, które wyglądają jak /person/123/dogspsy. Generalnie unikałem czegoś takiego, ponieważ na koniec sądzę, że tworząc taki identyfikator URI, uzyskujesz dostęp do kolekcji psów filtrowanej według identyfikatora konkretnej osoby. Byłoby to równoważne z /dogs?person=123. Czy naprawdę istnieje dobry powód, aby identyfikator URI REST miał głębokość większą niż dwa poziomy ( /collection/resource_id)?

Jeśli zagnieżdżona kolekcja jest naprawdę podfunkcją elementów członkowskich kolekcji zewnętrznej, uzasadnione jest, aby ustrukturyzować je jako pod-zasób. Przez „podfunkcję” rozumiem coś w rodzaju relacji składu UML, gdzie zniszczenie zewnętrznego zasobu naturalnie oznacza zniszczenie wewnętrznej kolekcji.

Inne typy kolekcji można modelować jako przekierowanie HTTP; dlatego /person/123/dogsrzeczywiście można na nie odpowiedzieć, wykonując 307, który przekierowuje do /dogs?person=123. W tym przypadku kolekcja nie jest tak naprawdę kompozycją UML, ale agregacją UML. Różnica ma znaczenie; to jest znaczące!

Donal Fellows
źródło
2
Masz ogólnie solidne punkty. Jednak chociaż resendEmailakcja mogłaby być obsadzona przez utworzenie kolekcji i wysłanie do niej, wydaje się to mniej naturalne. W rzeczywistości nie przechowuję niczego w bazie danych, gdy wiadomość e-mail jest wysyłana ponownie (bez potrzeby). Żaden zasób nie jest modyfikowany, dlatego jest to po prostu działanie, które się powiedzie lub nie. Nie mogłem zwrócić identyfikatora zasobu, który istnieje po zakończeniu połączenia, dzięki czemu taka implementacja stanie się hackiem, a nie RESTful. To po prostu nie jest operacja CRUD.
Justin Warkentin,
3

Zrozumiałe jest, że jestem trochę zdezorientowany, jak prawidłowo używać REST w oparciu o wszystkie sposoby, w jakie widziałem, jak duże firmy projektują swoje API REST.

Masz rację, ponieważ REST to system gromadzenia zasobów. Oznacza Reprezentatywne przekazanie państwa. Niezbyt dobra definicja, jeśli mnie o to poprosisz. Ale głównymi pojęciami są 4 VERB HTTP i bycie bezpaństwowcami.

Ważną rzeczą do zapamiętania jest to, że masz tylko 4 CZASOWNIKI z RESZTĄ. Są to GET, POST, PUT i DELETE. Twoim resendprzykładem byłoby dodanie nowego czasownika do REST. To powinna być czerwona flaga.

Pytanie 1

Ważne jest, aby zdawać sobie sprawę, że osoba dzwoniąca do interfejsu API REST nie powinna wiedzieć, że wykonanie PUTw kolekcji spowoduje wygenerowanie wiadomości e-mail. To dla mnie pachnie wyciekiem. Mogli wiedzieć, że wykonanie a PUTmoże spowodować dodatkowe zadania, które mogą później wykonać zapytanie. Wiedzieliby to, wykonując GETniedawno utworzony zasób. To GETzwróci zasób i wszystkie Taskpowiązane z nim identyfikatory zasobów. Następnie możesz później wysłać zapytanie do tych zadań, aby ustalić ich status, a nawet przesłać nowe Task.

Masz kilka opcji.

REST - podejście oparte na zasobach zadania

Utwórz taskszasób, w którym możesz przesyłać określone zadania do systemu w celu wykonywania działań. Możesz następnie GETwykonać zadanie na podstawie IDzwróconego zadania , aby ustalić jego status.

Możesz też połączyć SOAP over HTTPusługę internetową, aby dodać RPC do swojej architektury.

zapytania do wszystkich zadań dla określonego zasobu

GET http://server/api/myCollection/123/tasks

{ "tasks" :
    [ { "22333" : "http://server/api/tasks/223333" } ] 
}

przykład zasobu zadania

PUT http://server/api/tasks

{ 
    "type" : "send-email" , 
    "parameters" : 
    { 
         "collection-type" : "foo" , 
         "collection-id" : "123" 
    } 
}

==> zwraca identyfikator zadania

223334

GET http://server/api/tasks/223334

{ 
    "status" : "complete" , 
    "date" : "whenever" 
}

REST - używanie POST do wyzwalania akcji

Zawsze możesz dodać POSTdodatkowe dane do zasobu. Moim zdaniem naruszyłoby to ducha REST, ale nadal byłoby zgodne.

Możesz wykonać test POST podobny do tego:

POST http://server/api/collection/123

{ "action" : "send-email" }

Będziesz aktualizować zasób 123 z kolekcji o dodatkowe dane. Te dodatkowe dane są w zasadzie działaniem nakazującym backendowi wysłanie wiadomości e-mail dla tego zasobu.

Mam z tym problem, że a GETw zasobie zwróci te zaktualizowane dane. Jednak rozwiązałoby to Twoje wymagania i nadal byłoby ODPOWIEDNIE.

SOAP - usługa sieciowa, która akceptuje zasoby uzyskane z REST

Utwórz nową usługę WebService, w której możesz wysyłać wiadomości e-mail na podstawie poprzedniego identyfikatora zasobu z interfejsu API REST. Nie będę tutaj szczegółowo omawiał SOAP, ponieważ pierwotne pytanie dotyczy REST, a tych dwóch koncepcji / technologii nie należy porównywać, ponieważ są to jabłka i pomarańcze .

pytanie 2

Masz również kilka opcji tutaj:

Wydaje się, że wiele większych firm publikujących API REST udostępnia searchkolekcję, która jest tak naprawdę tylko sposobem na przekazanie parametrów zapytania w celu zwrócenia zasobów.

GET http://server/api/search?q="type = myCollection & someField >= someval"

Które zwróciłyby kolekcję w pełni kwalifikowanych zasobów REST, takich jak:

{
    "results" : 
       { [ 
             "location" : "http://server/api/myCollection/1",
             "location" : "http://server/api/myCollection/9",
             "location" : "http://server/api/myCollection/56"
         ]
       }
}

Lub możesz zezwolić na coś takiego jak MVEL jako parametr zapytania.

pytanie 3

Wolę pod-poziomy niż konieczność powrotu do poprzedniej wersji i zapytania do innego zasobu za pomocą parametru zapytania. Nie wierzę, że istnieje jakakolwiek zasada w taki czy inny sposób. Możesz zaimplementować oba sposoby i pozwolić dzwoniącemu zdecydować, który jest bardziej odpowiedni na podstawie tego, jak po raz pierwszy wszedł do systemu.

Notatki

Nie zgadzam się co do czytelności komentarzy innych osób. Pomimo tego, co niektórzy mogą sądzić, REST wciąż nie jest przeznaczony do spożycia przez ludzi. Przeznaczony jest na zużycie maszynowe. Jeśli chcę zobaczyć moje tweety, korzystam ze zwykłej strony internetowej Twittera. Nie wykonuję REST GET z ich API. Jeśli chcę programowo zrobić coś z moimi tweetami, używam ich interfejsu API REST. Tak, interfejsy API powinny być zrozumiałe, ale gtenie jest tak źle, po prostu nie jest intuicyjne.

Inną najważniejszą rzeczą w REST jest to, że powinieneś być w stanie rozpocząć w dowolnym punkcie API i przejść do wszystkich innych powiązanych zasobów BEZ znajomości dokładnego adresu URL innych zasobów z wyprzedzeniem. Wyniki GETVERB w REST powinny zwrócić pełny adres URL REST zasobów, do których się odwołuje. Zamiast zapytania zwracającego identyfikator Personobiektu zwróciłby w pełni kwalifikowany adres URL, taki jak http://server/api/people/13. Następnie zawsze możesz programowo nawigować po wynikach, nawet jeśli zmieni się adres URL.

Odpowiedź na komentarz

W prawdziwym świecie rzeczy, które muszą się wydarzyć, nie tworzą, nie czytają, nie aktualizują ani nie usuwają zasobów (CRUD).

Można podjąć dodatkowe działania dotyczące zasobów. Typowe relacyjne bazy danych obsługują koncepcję procedur przechowywanych. Są to dodatkowe polecenia, które można wykonać na zestawie danych. REST z natury nie ma tej koncepcji. I nie ma powodu, by tak było. Tego typu akcje są idealne dla RPC lub SOAP Web Services.

Jest to ogólny problem, który widzę z interfejsami API REST. Deweloperzy nie lubią ograniczeń koncepcyjnych otaczających REST, więc dostosowują go do robienia tego, co chcą. To jednak łamie go od bycia usługą RESTful. Zasadniczo te adresy URL stają się GETwywołaniami pseudo-REST-serwletów.

Masz kilka opcji:

  • Utwórz zasób zadania
  • Obsługa POSTdodatkowych danych do zasobu w celu wykonania akcji.
  • Dodaj dodatkowe polecenia za pośrednictwem usługi sieci Web SOAP.

Jeśli użyjesz parametru zapytania, którego VERB HTTP użyjesz do ponownego wysłania wiadomości e-mail?

  • GET- Czy to ponownie wysyła wiadomość e-mail ORAZ zwraca dane zasobu? Co jeśli system buforował ten adres URL i traktował go jak unikalny adres URL tego zasobu. Za każdym razem, gdy trafią w adres URL, ponownie wysyła wiadomość e-mail.
  • POST - W rzeczywistości nie wysłałeś żadnych nowych danych do zasobu, tylko dodatkowy parametr zapytania.

W oparciu o wszystkie podane wymagania, wykonanie POSTna zasobie z action fielddanymi POST rozwiąże problem.

Andrew T Finnell
źródło
3
Podczas gdy REST zaimplementowany przez HTTP daje te 4 czasowniki, nie jestem przekonany, że te czasowniki powinny być jego końcem. W prawdziwym świecie rzeczy, które muszą się wydarzyć, nie tworzą, nie czytają, nie aktualizują ani nie usuwają zasobów (CRUD). Ponowne wysłanie wiadomości e-mail to jedna z tych rzeczy. Nie muszę niczego przechowywać ani modyfikować w bazie danych. To po prostu działanie, które albo się powiedzie, albo nie powiedzie.
Justin Warkentin,
@JustinWarkentin Rozumiem, jakie są twoje potrzeby. Ale to nie czyni REST czymś, czym nie jest. Dodanie nowego czasownika do adresu URL jest niezgodne z architekturą REST. Zaktualizuję moją odpowiedź, aby zaoferować kolejną alternatywę, która byłaby ODPOCZNA.
Andrew T Finnell,
@JustinWarkentin Sprawdź w mojej odpowiedzi „REST - używanie POST do wyzwalania akcji”.
Andrew T Finnell,
0

Pytanie 1: Czy właściwe jest zmieszanie jakiegoś wywołania akcji z identyfikatorem URI zasobu [lub] czy lepiej byłoby określić akcję i przekazać identyfikator zasobu?

Dobre pytanie. W takim przypadku radzę zastosować to drugie podejście, a mianowicie określić akcję i przekazać identyfikator zasobu. W ten sposób, gdy twój zasób jest po raz pierwszy modyfikowany, z kolei wywołuje /sendEmailakcję (uwaga: nie trzeba nazywać go „ponownie wyślij”) jako osobne żądanie RESTful (które możesz później wywoływać wielokrotnie, niezależnie od modyfikowanego zasobu ).

Pytanie 2: dotyczące używania operatora porównania takiego:/collection?someField:gte=someval

Chociaż jest to technicznie w porządku, prawdopodobnie jest to zły pomysł. Jedną z kluczowych zasad REST jest czytelność. Sugerowałbym, aby po prostu przekazać operator porównania jako inny parametr, na przykład: /collection?someField=someval&operator=gtei oczywiście zaprojektować swój interfejs API, aby obsługiwał domyślny przypadek (w przypadku, gdy operatorparametr zostanie pominięty w URI).

Pytanie 3: Czy naprawdę istnieje dobry powód, aby identyfikator URI REST miał głębokość większą niż dwa poziomy?

Tak; dla abstrakcji. Widziałem kilka interfejsów API REST, które wykorzystują warstwy abstrakcji na wielu poziomach URI, na przykład: /vehicles/cars/123lub /vehicles/bikes/123które z kolei umożliwiają pracę z przydatnymi informacjami dotyczącymi zarówno kolekcji, jak /vehiclesi /vehicles/bikeskolekcji. Powiedziawszy to, nie jestem wielkim fanem tego podejścia; rzadko musisz to robić w praktyce, a istnieje szansa, że ​​możesz przeprojektować interfejs API, aby używał tylko 2 poziomów.

I tak, jak sugerują powyższe komentarze, w przyszłości najlepiej byłoby podzielić pytania na osobne posty;)

Kosta Kontos
źródło
Myślę, że mój przykład pytania nr 2 był zbyt uproszczony. Muszę określić operator porównania dla każdego pola używanego do filtrowania kolekcji, a nie tylko jednego, więc w twoim przykładzie musiałoby to być coś podobnego /collection?field1=someval&field1Operator=gte&field2=someval&field2Operator=eq.
Justin Warkentin,
0

W przypadku pytania 2 inna alternatywa może być bardziej elastyczna: rozważ każde wyszukiwanie jako zasób, który użytkownik buduje przed użyciem.

powiedzmy, że masz kontener „wyszukiwań”, tam robisz POST /api/searches/ze specyfikacją zapytania dla treści. może to być dokument JSON, XML lub nawet SQL, cokolwiek jest dla Ciebie łatwiejsze. Jeśli zapytanie zostanie poprawnie przeanalizowane, nowe wyszukiwanie zostanie utworzone jako nowy zasób z własnym identyfikatorem URI, powiedzmy/api/searches/q123/

Następnie klient może po prostu GET /api/searches/q123/pobrać wyniki zapytania.

Na koniec możesz poprosić klienta o usunięcie zapytania lub wyczyszczenie go po zamknięciu sesji.

Javier
źródło
0

Czy właściwe jest mieszanie jakiegoś wywołania akcji z identyfikatorem URI zasobu (np. / Collection / 123? Action = resendEmail)? Czy lepiej byłoby określić akcję i przekazać identyfikator zasobu (np. / Collection / resendEmail? Id = 123)? Czy to niewłaściwy sposób na to? Tradycyjnie (przynajmniej z HTTP) wykonywana akcja jest metodą żądania (GET, POST, PUT, DELETE), ale tak naprawdę nie pozwalają na niestandardowe akcje z zasobem.

Nie, nie jest to właściwe, ponieważ IRI służą do identyfikowania zasobów, a nie operacji (jednak ppl używa przez pewien czas metody zastępowania tej metody , w przypadkach gdy użycie metod innych niż POST i GET nie jest obsługiwane). Możesz poszukać odpowiedniej metody HTTP lub utworzyć nową. W takich przypadkach POST może być twoim przyjacielem (ppl użyj go, jeśli nie mogą znaleźć odpowiedniej metody, a żądanie nie jest pobierane). Inne podejście do tworzenia zasobów z wysyłania wiadomości e-mail, dzięki czemu POST /emailsmożna wysyłać wiadomości e-mail bez tworzenia prawdziwych zasobów. Btw. Struktura URI nie zawiera semantyki, więc z perspektywy REST tak naprawdę nie ma znaczenia, jakiego rodzaju URI używasz. Liczy się metadane (np. Relacja linków ) przypisane do linków wysłanych do klientów.

Najlepszym pomysłem, jaki do tej pory wymyśliłem, jest zezwolenie użytkownikowi interfejsu API na określenie go jako dodatku do nazwy pola (np. / Collection? SomeField: gte = someval - aby wskazać, że powinien zwrócić zasoby tam, gdzie someField jest większy niż lub równa (> =) cokolwiek, co jest. Czy to dobry pomysł? Zły pomysł? Jeśli tak, to dlaczego? Czy istnieje lepszy sposób, aby umożliwić użytkownikowi określenie rodzaju porównania do wykonania z danym polem i wartością?

Nie musisz tworzyć własnego języka zapytań. Wolę użyć już istniejącego i dodać opis zapytania do metadanych łącza. W tym celu należy użyć prawdopodobnie typu nośnika RDF (np. JSON-LD) lub niestandardowego typu MIME (jeśli nie ma formatu obsługującego ten format inny niż RDF). Korzystanie z istniejących standardów oddziela klienta od serwera, na tym polega jednolite ograniczenie interfejsu.

Byłoby to równoważne / psy? Osoba = 123. Czy naprawdę istnieje dobry powód, aby identyfikator URI REST miał głębokość większą niż dwa poziomy (/ collection / resource_id)?

Jak już wspomniałem, struktura URI nie ma znaczenia z perspektywy REST. Możesz użyć /x71fd823df2na przykład. Nadal miałoby to sens dla klientów, ponieważ sprawdzają metadane przypisane do łączy, a nie strukturę URI. Głównym celem identyfikatora URI jest identyfikacja zasobów. W standardzie URI stwierdzają, że ścieżka zawiera dane hierarchiczne, a zapytanie zawiera dane niehierarchiczne. Ale to, co jest hierarchiczne, może być bardzo subiektywne. Dlatego też możesz napotkać wiele poziomów głębokich identyfikatorów URI i identyfikatorów URI z długimi zapytaniami.

Szukałem bez końca od wielu godzin, ale nigdzie nie znalazłem odpowiedzi na moje pytania (może po prostu nie wiem, czego szukać?).

Powinieneś przeczytać przynajmniej ograniczenia REST z rozprawy Fielding , standardu HTTP i prawdopodobnie internetowych interfejsów API trzeciej generacji od Markusa.

inf3rno
źródło