Je cite la meilleure réponse :
I cannot point you to a tutorial, but can mention some things based on experience with writing RESTful services using Spring MVC.
split the Controllers from the business logic. Concerns to have in the Controllers: most of all error handling, also potentially authorization. The Controller methods might be quite thin initially, and just dispatch to corresponding business logic methods. That's not a bad thing, soon your Controllers will grow with issues of interfacing clients.
speaking of error handling, it is quite hard to get it right in a RESTful service. You definitely need to have a way to nicely inform the clients of errors, via structured data. That should be a part of all your responses, I guess. You will need to decide which errors you send back info about, which ones you are silent about and just log, etc.
most probably you will have some data objects for the requests you are getting, and the responses you are sending. Package them in a separate jar. Add to this jar interfaces, implemented by your Controllers. Add also a simple second implementation of these interfaces, that makes calls to your service. Here you go, you have a client lib. Distribute it, make your Java clients happy.
Even though now you have a nice Java client lib, do not forget to also test your service with curl, and document how to use it this way, with simple calls. Make your non-Java users happy.
There are all kinds of libs for "unit" testing Controllers, by mocking up more or less of the internals of a web server. These are very useful, but do not limit yourself to them. Have a qa env, where you fully deploy your service, and have a qa app which sends real fully fledged requests to the instance of your service on the qa env, and analyses their responses.
Try to keep things simple and consistent across the different calls. For example every response can contain the same "error" part with the same fields giving information in a structured programatically usable form about what went wrong.
REST is a nice abstraction, but has its limitation: in practice, /delete.json?id=3 can have very different effects on different services. Do not expect your clients to be able to guess what "add" and "delete" will mean in your particular case, as they will probably guess differently from what you expected. Instead, provide in your documentation some information about what your service will be doing under the hood. We are not yet at a stage where we are able to have components communicating via the knowledge of just a very thin interface, staying agnostic of their internals, unfortunately.
C'est très bien dit je trouve.
Avec une syntaxe proche de Sparkjava et Sinatra.
Aparté
Après plusieurs jours de recherche et quelques semaines de remise en question, je peux enfin mettre à plat ce que j'ai compris du clean code et surtout de ce que devrait être des TU écrits en TDD pour un service RESTful. Pour vous faire saisir l'idée dernière cela, je vais considérer que nous écrivons un serveur RESTful en Sparkjava (je n'ai pas encore migré totalement vers Javalin).
Principe de base
Selon Martin Fowler, Robert Martin et Kent Bent, nous n'avons pas forcément le lien MaClasse => MaClasseTest. En réalité, nous avons une problématique à laquelle répond une interface derrière laquelle se trouve une implémentation pouvant avoir un groupe de classes. Notre classe de test va donc tester ce petit groupe de classes à travers des TU.
Pourquoi ne pas conserver le pattern UneClasse <=> UneClasseTest ?
Simplement parce qu'il corrèle nos tests à la structure et l'organisation de notre code (ie. noms et packages). De manière formelle, on dit que le code des classes des tests et le code des sources sont covariants ; c'est-à-dire que changer l'architecture de l'un, induit inévitablement un changement chez l'autre.
Alors qu'en réalité, les TU écrits en TDD s'attachent à ce qui doit être fait et non comment cela est fait. De facto, tester l'implémentation à travers une interface décroit le couplage et rend le code de nos tests contra-variant avec le code de nos sources.
Débutons par un exemple
Considérons un micro-service RESTful qui gère un carnet d'adresse. La première question à se poser est : quelle est la liste des fonctionalités qu'expose ce service ?
Nous allons en considérer que deux :
- Ajouter un contact
- Supprimer un contact
=> Pas de recherche ni modification pour cet exemple.
En BDD, nous testerions ce service REST en lui envoyant du JSON ou carrément depuis l'interface graphique via du Selenium ou du Protractor où nos uses cases auraient été rédigés en Gherkin. Dans notre exemple en TDD, nous allons tester directement les méthodes des routes Sparkjava en mockant les paramètres spark.Request et spark.Response pour qu'ils retournent les valeurs qui nous arrangent bien.
Architecture applicative
L'achitecture demeure en couche, même si une encapsulation forte (au sens Yegor Bugayenko du terme) est omniprésente.
__________________
| |
| ROUTES |
| ^ |
|--------|---------
| v |
| PERSISTENCE |
| ^ |
|--------|--------|
| v |
| BASE DE DONNÉES |
|_________________|
Tests unitaires & TDD
Avant d'écrire la route AddPerson, nous allons commencer par écrire un test qui échoue :
/*
* COPYRIGHT © ITAMETIS - TOUS DROITS RÉSERVÉS
* Pour plus d'information veuillez contacter : copyright@itametis.com
*
* -------------------------------------------------------------------
*
* Cet extrait de code est tiré des dojos de nos amis chez ITAMETIS.
* Merci de m'avoir permise de m'en service ici pour étayer mon
* propos.
*/
import org.testng.Test
import org.assertj.Assertions.assertThat
import org.mockito.Mock.mock
import org.mockito.Mock.when
import spark.Request
import spark.Response
class AddPersonTest {
private val route:Route = AddPerson()
@Test
fun `AddPerson route should be able to create a regular user in base`() {
// Given
val json = "{'name':'Wayne', 'firstName':'Bruce', 'mobile':'+33012345678' }"
val request = mock(Request::class.java)
val response = mock(Response::class.java)
when(request.body()).thenReturn(json)
// When
val result = route.handle(request, response)
// Then
assertThat(result).contains("id")
.contains("'name':'Wayne'")
.contains("'firstName':'Bruce'")
.contains("'mobile':'+33012345678'")
}
}
Implémentation de la route AddPerson
Les routes sont des objets dédiés à une préoccupation. Typiquement la route AddPerson s'écrirait de la façon suivante :
/*
* COPYRIGHT © ITAMETIS - TOUS DROITS RÉSERVÉS
* Pour plus d'information veuillez contacter : copyright@itametis.com
*
* -------------------------------------------------------------------
*
* Cet extrait de code est tiré des dojos de nos amis chez ITAMETIS.
* Merci de m'avoir permise de m'en service ici pour étayer mon
* propos.
*/
import com.jsoniter.JsonIterator
import com.jsoniter.JsonStream
import spark.Request
import spark.Response
import com.itametis.sample.dto.PersonDto
import com.itametis.sample.entity.People
import com.itametis.sample.entity.People.Person
class AddPerson:Route {
companion object {
private const val CONTENT_TYPE = "Content-Type"
private const val RESPONSE_TYPE = "application/json"
}
data class PersonDto @JvmOverload construtor(
val id:Long? = null,
val name:String = "",
val firstName:String = "",
val mobile:String = "",
)
override fun handle(request:Request, response:Response):Any {
// Récupération du DTO
val dto:PersonDto = JsonIterator.deserialize(request.body(), Person::class.java)
// Enregistrement de la personne en base
val person:Person = People.createIt(
"name", dto.getName(),
"firstName", dto.getFirstName(),
"phone", dto.getPhone()
)
// Conversion en JSON de la personne fraîchement créée en base (avec son ID)
response.header(CONTENT_TYPE, RESPONSE_TYPE)
return JsonStream.serialize(person)
}
}
Quant à la déclaration de cette route dans Sparkjava, imaginons qu'elle soit accessible depuis deux URL :
- POST:/api/person/contact
- POST:/api/contact
Nous la déclarerions de la sorte :
/*
* COPYRIGHT © ITAMETIS - TOUS DROITS RÉSERVÉS
* Pour plus d'information veuillez contacter : copyright@itametis.com
*
* -------------------------------------------------------------------
*
* Cet extrait de code est tiré des dojos de nos amis chez ITAMETIS.
* Merci de m'avoir permise de m'en service ici pour étayer mon
* propos.
*/
class Main {
companion object {
@JvmStatic
fun main(vararg params:String) {
val addRoute:Route = AddPerson()
Sparkjava.post("/api/person/contact", addRoute)
Sparkjava.post("/api/contact", addRoute)
}
}
}
Que remarque-t-on ?
Avec cette architecture nous remarquons plusieurs choses :
- Que nos routes sont adhérentes à Sparkjava et que nous pourrions encapuler les objets Request et Response dans un objet à nous afin de se découpler de Sparkjava.
- Que la couche de service a été supprimée. Le code est intégralement remonté au niveau de la route. Cela fait sens puisque le contrôleur
RESTful se substitue aux Services de JEE dès que l'on a une SPA. - Qu'il est possible de factoriser les routes en leur affectant plusieurs URL.
- Que la route est porteuse de son DTO => Autre route => Autre contrat d'interfaçage => Autre DTO. On peut tout de même s'autoriser à
faire hériter nos routes d'une route abstraite.
Améliorations possibles
On peut améliorer les routes en les décorants par d'autres routes implémentant la même interface :
/*
* COPYRIGHT © ITAMETIS - TOUS DROITS RÉSERVÉS
* Pour plus d'information veuillez contacter : copyright@itametis.com
*
* -------------------------------------------------------------------
*
* Cet extrait de code est tiré des dojos de nos amis chez ITAMETIS.
* Merci de m'avoir permise de m'en service ici pour étayer mon
* propos.
*/
class Main {
companion object {
@JvmStatic
fun main(vararg params:String) {
val addRoute:Route = LogRouteInvocation( // On log la requête quoi qu'il arrive
AuthorizationChecker( // Est-on autorisé à exécuter la route
DtoToJsonConverter( // Évite le 'return JsonStream.serialize(...)' et le 'response.header(...)'
AddPerson() // La route d'origine
)
)
)
Sparkjava.post("/api/person/contact", addRoute)
Sparkjava.post("/api/contact", addRoute)
}
}
}
En gros avec REST HATEOAS, pour chaque URL interrogée, le serveur traite la requête et renvoie le résultat + la liste des URL possibles contenant les infos qu'il n'a pas mais utiles à cette requête (découverte incrémental de l'API). J'aime bien l'idée.
Comprendre RESTfull en 2-3 minutes.
En résumé :
1) RESTful s'appuie sur une syntaxe particulière pour les URL. Je cite :
<<
/api/users when called with GET, lists users
/api/users when called with POST, creates user record
/api/users/1 when called with GET, shows user record
when called with PUT, updates user record
when called with DELETE, deletes user record
when called with PATCH, (met à jour uniquement la valeur nécessaire, par obligé de repasser toute l'entité en entier mais juste les changements)
2) RESTful est statelesss
3) Il y a 5 verb dans rest