REST API - Métodos HTTP implementados em Spring Boot
As APIs REST permitem que você desenvolva qualquer tipo de aplicativo da web com todas as operações CRUD (criar, recuperar, atualizar, excluir) possíveis. As diretrizes REST sugerem o uso de um método HTTP específico em um tipo particular de chamada feita ao servidor (embora tecnicamente seja possível violar essa diretriz, ainda assim é altamente desencorajado).
REST e RESTful são a mesma coisa, ambos representando os mesmos princípios, existe apenas uma diferença gramatical. Sistemas que utilizam REST são chamados de RESTful.
-
REST: Conjunto de princípios de arquitetura.
-
RESTful: Capacidade de determinado sistema aplicar os princípios de REST.
Use as informações fornecidas abaixo para encontrar um método HTTP adequado para a ação executada pela API.
GET
Faça requisições GET apenas para recuperar informações de um recurso e não para modificá-las. Requisições GET são consideradas seguras, pois não devem alterar nenhum recurso. APIs GET devem ser idempotentes, o que significa que ao realizarmos varias requisições idênticas o mesmo resultado deve ser obtido, até que outra requisição (POST, PUT ou PATCH) altere o estado do recurso.
Regras
-
Quando o recurso for encontrado o código da reposta HTTP deve ser 200 (OK), junto com o corpo da resposta.
-
Caso o recurso não seja encontrado, o código da reposta HTTP deve ser 404 (NOT FOUND).
-
Mas quando o formato da requisição não está correto, o servidor deve retornar o código HTTP 400 (BAD REQUEST).
Exemplo de URIs
-
HTTP GET http://localhost:8080/authors
-
HTTP GET http://localhost:8080/authors/1
-
HTTP GET http://localhost:8080/authors/2000
POST
Ao falar estritamente em termos de REST, os métodos POST são usados para criar um novo recurso na coleção de recursos. Diferente do verbo GET o POST não é seguro nem idempotente, o que significa que ao realizarmos varias requisições idênticas resultada em recursos diferentes contendo as mesmas informações, com exceções dos identificadores do recurso.
Regras
-
Quando um recurso for criado no servidor o código da resposta HTTP deve ser 201 (CREATED), o recurso criado deve ser retornado no corpo da requisição, e deve conter os links de acessos para consulta desse recurso, mas caso a aplicação não disponibilize de um endpoint para consulta do recurso, o código HTTP retornado deve ser 200 (OK), e caso nenhum recurso seja retornado no corpo da requisição a resposta deve ser 204 (NO CONTENT).
-
Caso o recurso recebido no corpo da requisição não esteja de acordo com o esperado pelo servidor, o código da resposta HTTP deve ser 400 (BAD REQUEST).
Exemplo de URIs
-
HTTP POST http://localhost:8080/authors --data
{"email": "gasparbarancelli@gaspar.com", "name": "gaspar"}
-
HTTP POST http://localhost:8080/authors --data
{"email": "gasparbarancelli@gaspar.com"}
PUT
Faça requisições PUT para atualizar todo o recurso existente.
Regras
-
Quando o recurso for encontrado e atualizado o código da resposta HTTP deve ser 200(OK), o recurso atualizado deve ser retornado no corpo da requisição, e deve conter os links de acessos para consulta desse recurso.
-
Caso o recurso não seja encontrado, o código da reposta HTTP deve ser 404 (NOT FOUND).
-
Caso o recurso recebido no corpo da requisição não esteja de acordo com o esperado pelo servidor, o código da resposta HTTP deve ser 400 (BAD REQUEST);
Exemplo de URIs
-
HTTP PUT http://localhost:8080/authors/1 --data
{"email": "gaspar@gaspar.com", "name": "gaspar barancelli"}
-
HTTP PUT http://localhost:8080/authors/1000 --data
{"email": "gaspar@gaspar.com", "name": "gaspar barancelli"}
-
HTTP PUT http://localhost:8080/authors/1 --data
{"email": "gaspar@gaspar.com"}
DELETE
Requisições feitas para o verbo DELETE são para excluir um recurso.
Regras
-
Se o recurso existir e a operação for bem sucedida e nenhum recurso precisa ser retornado o código HTTP retornado deve ser 204 (NO CONTENT), mas caso um recurso seja retornado o código HTTP deve ser 200(OK), e o recurso deve ser adicionado no corpo da resposta.
-
Caso o recurso não seja encontrado, o código da reposta HTTP deve ser 404 (NOT FOUND).
Exemplo de URIs
-
HTTP DELETE http://localhost:8080/authors/1
-
HTTP DELETE http://localhost:8080/authors/1000
PATCH
As requisições PATCH devem fazer uma atualização parcial em um recurso. Comentamos anteriormente que o verbo PUT altera totalmente um recurso, o PATCH deve ser utilizado quando devemos alterar partes do recurso.
Regras
-
Quando o recurso for encontrado e atualizado o código da resposta HTTP deve ser 200 (OK), o recurso atualizado deve ser retornado no corpo da requisição, e deve conter os links de acessos para consulta desse recurso.
-
Caso o recurso não seja encontrado, o código da reposta HTTP deve ser 404 (NOT FOUND).
-
Caso o recurso recebido no corpo da requisição não esteja de acordo com o esperado pelo servidor, o código da resposta HTTP deve ser 400 (BAD REQUEST);
Exemplo de URIs
-
HTTP PATCH http://localhost:8080/authors/1 --data
{"email": "gasparbarancelli@gaspar.com", "name": "junior", "linkedIn": "urllinkedin", "faceBook": "urlfaceBook"}
-
HTTP PATCH http://localhost:8080/authors/1000 --data
{"email": "gasparbarancelli@gaspar.com", "name": "junior", "linkedIn": "urllinkedin", "faceBook": "urlfaceBook"}
-
HTTP PATCH http://localhost:8080/authors/1 --data
{"email": "gaspar@gaspar.com"}
Aplicação de demonstração
Vamos criar uma aplicação utilizando o Spring Boot, na qual vamos manipular um recurso de Author.
Nossa aplicação vai fazer uso das seguintes dependências.
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>2.3.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
Nesta aplicação vamos persistir nosso recurso no banco de dados H2. Segue as configuração que devem ser atribuídas no arquivo
.application.properties
# DATASOURCE
spring.datasource.url=jdbc:h2:file:./data/exemplo
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
# H2 Console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# JPA
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
Por padrão o módulo Web do Spring Boot utiliza a biblioteca do Jackson para serialização de Json, mas vamos adicionar um novo modulo para que o Jackson possa nos informar quando um campo exibir no corpo de uma requisição, utilizaremos isso mais a frente no exemplo do método Patch.
import com.fasterxml.jackson.databind.ObjectMapper;
import org.openapitools.jackson.nullable.JsonNullableModule;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
public class JacksonConfiguration {
private final ObjectMapper objectMapper;
public JacksonConfiguration(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@PostConstruct
public ObjectMapper jacksonObjectMapper() {
objectMapper.registerModule(new JsonNullableModule());
return objectMapper;
}
}
Nosso próximo passo é configurar o ModelMapper, uma biblioteca responsável por converter objetos, vamos utilizar esta biblioteca nos métodos POST e PUT, onde recebemos um DTO no corpo da requisição e os convertemos para nossa entidade.
import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ModelMapperConfiguration {
@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
}
Agora vamos criar uma Exception que será lançada quando um recurso de Autor não for encontrado.
import javax.persistence.NoResultException;
public class AuthorNotFoundException extends NoResultException {
AuthorNotFoundException(Long id) {
super("Could not find author " + id);
}
}
Abaixo segue a configuração do nosso
ele é responsável por interceptar os erros da nossa API.ControllerAdvice
Como foi comentado anteriormente quando um recurso não for encontrado será lançada a exceção
, e quando essa exceção for lançada ela será interceptada para que o código da resposta HTTP seja 404 (NOT FOUND).AuthorNotFoundException
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import javax.persistence.NoResultException;
@ControllerAdvice
public class ExceptionAdviceConfiguration {
@ResponseBody
@ExceptionHandler(NoResultException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String noResultExceptionHandler(NoResultException e) {
return e.getMessage();
}
}
Nossa entidade deve aplicar algumas validações, como boa prática separei em uma nova classe.
package com.gasparbarancelli.restfulapi.validator;
import java.util.function.Predicate;
import java.util.regex.Pattern;
public class Validator {
public static Predicate<String> isEmail = email -> {
var emailValidationRegex = "^(.+)@(.+)$";
var pattern = Pattern.compile(emailValidationRegex);
return pattern.matcher(email).matches();
};
public static Predicate<String> isNotEmpty = value -> value != null && value.trim().length() > 0;
}
Vamos a implementação da nossa entidade Author, é esse objeto que vamos persistir no banco de dados. Observe a implementação do mesmo, onde não definimos nenhuma forma de atribuição ao identificador da entidade, ele só vai ser atribuído pelo JPA ou pelo Jackson. Também criamos um builder para facilitar a criação de uma nova instância desse objeto.
import com.gasparbarancelli.restfulapi.validator.Validator;
import org.springframework.lang.NonNull;
import javax.persistence.*;
import java.util.Objects;
@Entity
@Table(name = "AUTHOR")
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "NAME", nullable = false, length = 50)
private String name;
@Column(name = "EMAIL", nullable = false, length = 320)
private String email;
@Column(name = "LINKEDIN", length = 100)
private String linkedIn;
@Column(name = "FACEBOOK", length = 100)
private String faceBook;
@Column(name = "TWITTER", length = 100)
private String twitter;
@Column(name = "SUMMARY")
private String summary;
@Deprecated
public Author() {
}
private Author(@NonNull String name, @NonNull String email) {
setName(name);
setEmail(email);
}
public static AuthorBuilder builder(@NonNull String name, @NonNull String email) {
return new AuthorBuilder(name, email);
}
public static class AuthorBuilder {
private final Author author;
public AuthorBuilder(@NonNull String name, @NonNull String email) {
this.author = new Author(name, email);
}
public AuthorBuilder linkedIn(String linkedIn) {
this.author.linkedIn = linkedIn;
return this;
}
public AuthorBuilder faceBook(String faceBook) {
this.author.faceBook = faceBook;
return this;
}
public AuthorBuilder twitter(String twitter) {
this.author.twitter = twitter;
return this;
}
public AuthorBuilder summary(String summary) {
this.author.summary = summary;
return this;
}
public Author build() {
return this.author;
}
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
public String getLinkedIn() {
return linkedIn;
}
public String getFaceBook() {
return faceBook;
}
public String getTwitter() {
return twitter;
}
public String getSummary() {
return summary;
}
public void setName(String name) {
this.name = Objects.requireNonNull(name, "name must not be null");
}
public void setEmail(String email) {
this.email = Objects.requireNonNull(email, "email must not be null");
if (!Validator.isNotEmpty.and(Validator.isEmail).test(email)) {
throw new IllegalArgumentException("email is not valid");
}
}
public void setLinkedIn(String linkedIn) {
this.linkedIn = linkedIn;
}
public void setFaceBook(String faceBook) {
this.faceBook = faceBook;
}
public void setTwitter(String twitter) {
this.twitter = twitter;
}
public void setSummary(String summary) {
this.summary = summary;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Author author = (Author) o;
return name.equals(author.name) && email.equals(author.email);
}
@Override
public int hashCode() {
return Objects.hash(name, email);
}
}
Como foi comentado anteriormente vamos receber um DTO no corpo das requisições POST e PUT, abaixo segue a sua implementação.
import javax.validation.constraints.NotNull;
public class AuthorPersist {
@NotNull
private String name;
@NotNull
private String email;
private String linkedIn;
private String faceBook;
private String twitter;
private String summary;
@Deprecated
public AuthorPersist() {
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
public String getLinkedIn() {
return linkedIn;
}
public String getFaceBook() {
return faceBook;
}
public String getTwitter() {
return twitter;
}
public String getSummary() {
return summary;
}
}
Segue a implementação do DTO utilizado nas requisições PATCH, observe a utilização do objeto
, ele vai ser capaz de nos informar se a propriedade foi recebida no corpo da requisição.JsonNullable
import org.openapitools.jackson.nullable.JsonNullable;
import javax.validation.constraints.NotNull;
public class AuthorUpdateDto {
@NotNull
private final JsonNullable<String> name = JsonNullable.undefined();
@NotNull
private final JsonNullable<String> email = JsonNullable.undefined();
private final JsonNullable<String> linkedIn = JsonNullable.undefined();
private final JsonNullable<String> faceBook = JsonNullable.undefined();
private final JsonNullable<String> twitter = JsonNullable.undefined();
private final JsonNullable<String> summary = JsonNullable.undefined();
@Deprecated
public AuthorUpdateDto() {
}
public JsonNullable<String> getName() {
return name;
}
public JsonNullable<String> getEmail() {
return email;
}
public JsonNullable<String> getLinkedIn() {
return linkedIn;
}
public JsonNullable<String> getFaceBook() {
return faceBook;
}
public JsonNullable<String> getTwitter() {
return twitter;
}
public JsonNullable<String> getSummary() {
return summary;
}
}
Nossa aplicação deve ser capaz de trabalhar com HATEOAS, como mencionado nas explicações dos métodos, algumas requisições devem retornar um link para que seja possível consultar o recurso, então a classe abaixo deve ser criada para que possamos aplicar HATEOAS na resposta do objeto Author.
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.RepresentationModelAssembler;
import org.springframework.stereotype.Component;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
@Component
class AuthorModelAssembler implements RepresentationModelAssembler<Author, EntityModel<Author>> {
@Override
public EntityModel<Author> toModel(Author author) {
return EntityModel.of(author,
linkTo(methodOn(AuthorController.class).one(author.getId())).withSelfRel(),
linkTo(methodOn(AuthorController.class).all(null, null)).withRel("authors"));
}
}
Em nossa aplicação de exemplo vamos persistir os nossos recursos no banco de dados, então vamos utilizar o Spring Data para facilitar essa comunicação com o banco de dados, abaixo segue a implementação do JpaRepository, que vai nos fornecer diversos métodos para manipulação do nosso recurso com o banco de dados.
import org.springframework.data.jpa.repository.JpaRepository;
public interface AuthorRepository extends JpaRepository<Author, Long> {
}
Para separarmos um pouco mais as responsabilidades dos nossos objetos, vamos criar um serviço que vai fazer o meio de campo com as chamadas de APIs e nosso banco de dados.
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.lang.NonNull;
public interface AuthorService {
Author findByIdOrElseThrow(@NonNull Long id);
Author save(@NonNull Author author);
Page<Author> findAll(@NonNull PageRequest pageable);
void deleteByIdOrElseThrow(@NonNull Long id);
}
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
@Service
public class AuthorServiceImpl implements AuthorService {
private final AuthorRepository authorRepository;
public AuthorServiceImpl(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
@Override
public Author findByIdOrElseThrow(@NonNull Long id) {
return authorRepository.findById(id)
.orElseThrow(() -> new AuthorNotFoundException(id));
}
@Override
public Author save(@NonNull Author author) {
return authorRepository.save(author);
}
@Override
public Page<Author> findAll(@NonNull PageRequest pageable) {
return authorRepository.findAll(pageable);
}
@Override
public void deleteByIdOrElseThrow(@NonNull Long id) {
if (authorRepository.existsById(id)) {
authorRepository.deleteById(id);
} else {
throw new AuthorNotFoundException(id);
}
}
}
Para evitar a repetição do código a ser aplicado em nosso método Patch, vamos criar uma classe útil para verificar se determinada propriedade foi recebida no corpo da nossa requisição, e caso tenha sido recebida, podemos efetuar uma determinada ação.
import org.openapitools.jackson.nullable.JsonNullable;
import java.util.function.Consumer;
public class JsonUtil {
public static <T> void changeIfPresent(JsonNullable<T> nullable, Consumer<T> consumer) {
if (nullable.isPresent()) {
consumer.accept(nullable.get());
}
}
}
Por fim, segue a implementação da nossa API, onde vamos implementar todos os métodos HTTP explicados no inicio deste post.
import com.gasparbarancelli.restfulapi.author.dto.AuthorPersist;
import com.gasparbarancelli.restfulapi.author.dto.AuthorUpdateDto;
import com.gasparbarancelli.restfulapi.util.JsonUtil;
import org.modelmapper.ModelMapper;
import org.springframework.data.domain.PageRequest;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.stream.Collectors;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
@RestController
@RequestMapping("authors")
public class AuthorController {
private final AuthorService authorService;
private final AuthorModelAssembler authorModelAssembler;
private final ModelMapper modelMapper;
public AuthorController(AuthorService authorService, AuthorModelAssembler authorModelAssembler, ModelMapper modelMapper) {
this.authorService = authorService;
this.authorModelAssembler = authorModelAssembler;
this.modelMapper = modelMapper;
}
@GetMapping
public CollectionModel<EntityModel<Author>> all(
@NonNull @RequestParam(value = "size", defaultValue = "10", required = false) Integer size,
@NonNull @RequestParam(value = "page", defaultValue = "0", required = false) Integer page) {
var pageable = PageRequest.of(page, size);
var authors = authorService.findAll(pageable).stream()
.map(authorModelAssembler::toModel)
.collect(Collectors.toList());
return CollectionModel.of(authors, linkTo(methodOn(AuthorController.class).all(size, page))
.withSelfRel());
}
@GetMapping("{id}")
public EntityModel<Author> one(@NonNull @PathVariable("id") Long id) {
var author = authorService.findByIdOrElseThrow(id);
return authorModelAssembler.toModel(author);
}
@PostMapping
public ResponseEntity<?> newAuthor(@NonNull @Valid @RequestBody AuthorPersist authorPersist) {
var author = modelMapper.map(authorPersist, Author.class);
var entityModel = authorModelAssembler.toModel(authorService.save(author));
return ResponseEntity
.created(entityModel.getRequiredLink(IanaLinkRelations.SELF).toUri())
.body(entityModel);
}
@PutMapping("{id}")
public ResponseEntity<?> update(
@NonNull @Valid @RequestBody AuthorPersist authorPersist,
@NonNull @PathVariable Long id) {
var author = authorService.findByIdOrElseThrow(id);
modelMapper.map(authorPersist, author);
var entityModel = authorModelAssembler.toModel(authorService.save(author));
return ResponseEntity.ok(entityModel);
}
@DeleteMapping("{id}")
public ResponseEntity<?> delete(@NonNull @PathVariable Long id) {
authorService.deleteByIdOrElseThrow(id);
return ResponseEntity.noContent().build();
}
@PatchMapping("{id}")
public ResponseEntity<?> updateAuthor(
@NonNull @PathVariable("id") Long id,
@Valid @RequestBody AuthorUpdateDto authorUpdateDto) {
Author author = authorService.findByIdOrElseThrow(id);
author.setName(authorUpdateDto.getName().get());
author.setEmail(authorUpdateDto.getEmail().get());
JsonUtil.changeIfPresent(authorUpdateDto.getLinkedIn(), author::setLinkedIn);
JsonUtil.changeIfPresent(authorUpdateDto.getFaceBook(), author::setFaceBook);
JsonUtil.changeIfPresent(authorUpdateDto.getTwitter(), author::setTwitter);
JsonUtil.changeIfPresent(authorUpdateDto.getSummary(), author::setSummary);
var entityModel = authorModelAssembler.toModel(authorService.save(author));
return ResponseEntity.ok(entityModel);
}
}
O código fonte dessa aplicação esta no repositório hospedado no GitHub.
TESTES

Método POST, adicionado um novo recurso

Método PUT, editando completamente o recurso

Método GET, obtendo um recurso em especifico

Método GET, obtendo uma lista de recursos

Método PATCH, editando alguns atributos do recurso

Teste método DELETE, excluindo o recurso