Cómo devolver el resultado de un POST en un servicio REST

En varios posts he implementado, con un framework u otro, un servicio REST normalmente muy sencillo. Al ser tan sencillos normalmente no cumplo al 100% las «especificaciones» o buenas prácticas que se recomiendan para considerar un servicio como REST. En este post voy a explicar como se debe implementar un POST, el resultado que devolvemos en él y en concreto el modo de hacerlo en Java.

Concepto:

En la mayoría de ejemplos de servicios REST que he implementado en este blog, para no complicar el código, lo que hago es simplemente devolver un 201 (CREATED). Incluso en algunos casos devuelvo en el body el objeto completo recién creado. Pero lo más correcto es no devolver nada en el ‘body’, y devolver en el ‘location’ de los ‘headers’ la url con la que acceder a ese recurso recién creado. La respuesta debería ser algo así:

HTTP/1.1 201 Created
Server: Apache-Coyote/1.1
Location: http://localhost:8080/api/info/12345
Content-Length: 0

Vamos a ver como devolver esta respuesta tanto en un proyecto con Spring MVC como en un proyecto con JAX-RS. Para ello modificaré los REST que hice para los posts de implementar un servicio Rest con Spring e implementar un servicio Rest con JAX-RS (Jersey).

Por último, comentaremos brevemente como sería si usáramos Spring HATEOAS.

Entorno usado:
Java JDK 1.8
Maven 3.2.1
Git 1.9.5
IDE Intellij 14.1.1 Ultimate version

Con Spring MVC:

1. Usaremos el proyecto spring-rest-example el cual implementé en mi post Implementar un servicio Rest con Spring. Si abrimos nuestro controller, veremos que en el caso del POST no devolvemos nada:

    @RequestMapping(method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.CREATED)
    public void insertUser(@RequestBody User user) {
        userService.save(user);
    }

2. Lo que necesitamos es devolver en los headers el location relleno con la url del objeto creado, sería algo así:

    @RequestMapping(method = RequestMethod.POST)
    public ResponseEntity<User> insertUser(@RequestBody User user) {
        User userSaved = userService.save(user);
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setLocation(...);

        return new ResponseEntity<>(httpHeaders, HttpStatus.CREATED);
    }

Creamos un objeto HttpHeaders, y necesitamos setearle el location (de tipo URI). Y en el return del método devolvemos un ResponseEntity con esos headers y el código HTTP de respuesta que corresponde a un POST (201 – Created).

La primera idea que se nos puede venir a la cabeza es meterle la url directamente, con un String o quizá obteniendo el dominio de alguna variable o propiedad, que tengamos en una constante. Con esto funcionaria, pero… vamos a hacerlo un poco mejor 🙂

3. Usaremos un builder que nos provee Spring para crear URIs: ServletUriComponentsBuilder, de la siguiente manera:

    @RequestMapping(method = RequestMethod.POST)
    public ResponseEntity<User> insertUser(@RequestBody User user) {
        User userSaved = userService.save(user);
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setLocation(
                ServletUriComponentsBuilder
                        .fromCurrentRequest()
                        .path("/{id}")
                        .buildAndExpand(userSaved.getId())
                        .toUri());

        return new ResponseEntity<>(httpHeaders, HttpStatus.CREATED);
    }

Con esto creamos nuestra URI, los métodos del builder son bastante autoexplicativos: con la request actual, le añadimos un path variable ‘/{id}’, y le metemos el id en esa variable.
(El código en mi github está algo refactorizado, pero es lo mismo 😛 )

4. Arrancando con el plugin maven de Spring Boot, como siempre, podemos probar nuestro POST. La respuesta del POST /api/users será algo así:

HTTP/1.1 201 Created
Server: Apache-Coyote/1.1
Location: http://localhost:8080/api/users/45332
Content-Length: 0
Date: Mon, 06 Apr 2015 18:23:50 GMT

5. (Bonus) Testing: Si tenemos test unitario de nuestro controller, se complica un poco. Tenemos que, o bien mockear los métodos del builder (es estático, necesitaríamos PowerMock o similar), o simplemente añadimos una MockHttpServletRequest al contexto, cosa que es lo que he hecho en el método @Before, para no complicarme mucho. El test completo, aquí
Si lo que tenemos son tests de integración, no habría que cambiar nada si el test está bien configurado.

Con JAX-RS (Jersey):

1. Usaremos el proyecto jersey-rest-example el cual implementé en mi post Implementar un servicio REST con JAX-RS (Jersey). Si abrimos nuestro controller, ahora mismo tenemos lo siguiente:

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public Response insertUser(User user) {
        userService.save(user);
        return Response.status(Response.Status.CREATED).build();
    }

2. Necesitamos, igual que antes devolver en los headers el location relleno con la url de nuestro recurso, en este caso, de esta manera:

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public Response insertUser(User user) {
        User userSaved = userService.save(user);
        URI uri = ...;
        return Response.created(uri).build();
    }

Aquí devolvemos un Response, que creamos con su método ‘created’ (esto le asigna ya como http status el 201 – Created) y le pasamos la uri que se asignará en el location.

3. Y al igual que antes, vamos a hacerlo con un builder que nos de la url, en este caso usaremos uno de la especificación JAX-RS: UriBuilder. Para ello, eso sí, necesitamos inyectar en nuestro controller un UriInfo, que se obtiene del contexto de la aplicación:

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public Response insertUser(User user) {
        User userSaved = userService.save(user);
        UriBuilder uriBuilder = uriInfo.getRequestUriBuilder();
        URI uri = uriBuilder.path(userSaved.getId().toString()).build();
        return Response.created(uri).build();
    }

Necesitamos un bean de contexto, UriInfo, con el que podremos obtener el builder concreto para nuestra URI. una vez con el builder, con su método ‘path’ le añadimos el id de nuestro recurso, y listo.
(El código en mi github está algo refactorizado, pero es lo mismo 😛 )

4. Arrancamos la aplicación (ya sea con un tomcat o con un glassfish, como vimos en el post de JAX-RS), y el servicio nos devolverá lo siguiente al hacer el POST:

HTTP/1.1 201 Created
Server: GlassFish Server Open Source Edition  4.1
X-Powered-By: Servlet/3.1 JSP/2.3 (GlassFish Server Open Source Edition  4.1  Java/Oracle Corporation/1.8)
Location: http://localhost:8080/api/users/45332
Date: Mon, 06 Apr 2015 18:50:13 GMT
Content-Length: 0

5. (Bonus) Testing: aquí, si también tenemos test unitario, igualmente se complica un poco. En este caso si he mockeado todo el tema de la UriInfo. El tests completo, aquí.

Con Spring HATEOAS:

No quiero entrar mucho en detalle, porque creo que esto da para otro post aparte que escribiré pronto, pero no quería terminar este post sin nombrar el concepto HATEOAS, así como el modulo de Spring que lo implementa. Si usamos Spring HATEOAS en nuestro proyecto, no necesitamos obtener el link a través de ningún builder, ya que nuestro propio recurso tendrá su link «self». Más información sobre HATEOAS aquí, así como sobre el proyecto Spring. Lo veremos en otro post.

Nada más. Recordad que todo el código de este post está subido en los proyectos spring-rest-example y jersey-rest-example de mi github.