Building a REST API in Play Framework
pazvanti
Posted on April 21, 2021
Article originally posted on my personal website under Building a REST API in Play Framework. Sample requests can also be found there
When building web applications, REST has become the most widely used approach because it is easy to use and easy to implement. In this tutorial I will be showing how to build a simple REST API in Play Framework and will cover the 4 basic functionalities:
- Create – POST
- Update – PUT
- Retrieve – GET
- Remove – DELETE
We will be creating a simple API for a student’s management software where we can manipulate the students and retrieve the stored ones. It won’t be anything too fancy, but an introduction to Play Framework and how a REST API can be build.
Setting up our Entities and DB
First, let’s create our database. We will store the student’s name, year in the university and the department he is currently studying in. It is a simple structure with only 4 columns (there is also an ID) and nothing more.
Our entity class looks like this. This will tie the database table to our application model and will allow us to easily map from the DB to a Java class and vice-versa.
package data.domain;
import javax.persistence.*;
@Entity
@Table(name = "students")
public class StudentDO {
@Id
@Column
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
@Column
private String name;
@Column
private Integer year;
@Column
private String department;
}
Building our DAO
Now that we have the Entity, we need to be able to actually store the data in the database and retrieve it when needed. This is done by the JPA Api and Play makes it easy to use it. We will be creating our DAO class that will provide the needed methods for the CRUD operations. CRUD stands for Create, Read, Update and Delete, so these are the methods that we will be implementing.
import com.google.inject.Inject;
import data.domain.StudentDO;
import play.db.jpa.JPAApi;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import java.util.List;
public class StudentDao {
private static final String ENTITY_MANAGER_NAME = "default";
@Inject
protected JPAApi jpaApi;
public StudentDO create(StudentDO studentDO) {
jpaApi.withTransaction(entityManager -> { entityManager.persist(studentDO); });
return studentDO;
}
public StudentDO find(Integer id) {
return jpaApi.em(ENTITY_MANAGER_NAME).find(StudentDO.class, id);
}
public List<StudentDO> find() {
EntityManager entityManager = jpaApi.em(ENTITY_MANAGER_NAME);
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<StudentDO> criteriaQuery = criteriaBuilder.createQuery(StudentDO.class);
Root<StudentDO> root = criteriaQuery.from(StudentDO.class);
criteriaQuery.select(root);
return entityManager.createQuery(criteriaQuery).getResultList();
}
public void delete(int id) {
jpaApi.withTransaction(entityManager -> {
StudentDO studentDO = entityManager.find(StudentDO.class, id);
if (studentDO != null) {
entityManager.remove(studentDO);
}
});
}
public StudentDO update(StudentDO studentDO) {
jpaApi.withTransaction(entityManager -> {entityManager.merge(studentDO);});
return studentDO;
}
}
We also implemented a method for retrieving all the students that are saved. A more advanced system will also provide filtering options, pagination and other features that are mandatory for an advanced web app, but this will do for our simple REST API tutorial.
The Service and transfer objects
The controller should not communicate directly with the DAO. We should have separation of functionality and each layer has it’s own role. The DAO communicates with the database, the Service has the business logic and the Controller is for external access. I won’t go into details on this topic, but I usually structure my code like this and it is easier to maintain when you know who is responsible for what.
Also, for more advanced systems you should not return directly the DO to the outside. Instead, we should have a mapping between the Domain object and an externally-used Transfer object. This DTO (Data Transfer Object) is the external representation, while the DO is the internal one. For this simple system, there are no differences (so I won’t include the DTO class), but more complex ones usually do have, with the DTO being composed of the data from multiple DOs and with new fields that are processing results.
First, let’s create a utility class that maps between Transfer and Domain objects. Nothing to fancy and if you desire you can use BeanUtils to achieve this.
import data.domain.StudentDO;
import data.dto.StudentDTO;
public class StudentMapper {
public static StudentDTO toTransfer(StudentDO domain) {
StudentDTO dto = new StudentDTO();
dto.setId(domain.getId());
dto.setName(domain.getName());
dto.setYear(domain.getYear());
dto.setDepartment(domain.getDepartment());
return dto;
}
public static StudentDO fromTransfer(StudentDTO dto) {
StudentDO domain = new StudentDO();
domain.setId(dto.getId());
domain.setName(dto.getName());
domain.setYear(dto.getYear());
domain.setDepartment(dto.getDepartment());
return domain;
}
}
Next, our service that handles all the business logic. Again, there is nothing fancy going on in this simple example. We get the DTO, map it to a DO, call the DAO and return the result.
import com.google.inject.Inject;
import data.dao.StudentDao;
import data.domain.StudentDO;
import data.dto.StudentDTO;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class StudentService {
@Inject
private StudentDao studentDao;
public List<StudentDTO> getAll() {
List<StudentDO> studentDOList = studentDao.find();
return studentDOList.stream().map(domain -> StudentMapper.toTransfer(domain)).collect(Collectors.toList());
}
public Optional<StudentDTO> getById(int id) {
StudentDO studentDO = studentDao.find(id);
if (studentDO == null) {
return Optional.empty();
}
return Optional.of(StudentMapper.toTransfer(studentDO));
}
public StudentDTO create(StudentDTO studentDTO) {
StudentDO studentDO = StudentMapper.fromTransfer(studentDTO);
studentDO = studentDao.create(studentDO);
return StudentMapper.toTransfer(studentDO);
}
public Optional<StudentDTO> update(StudentDTO studentDTO, int id) {
StudentDO fromDb = studentDao.find(id);
if (fromDb == null) {
return Optional.empty();
}
fromDb.setName(studentDTO.getName());
fromDb.setYear(studentDTO.getYear());
fromDb.setDepartment(studentDTO.getDepartment());
fromDb = studentDao.update(fromDb);
return Optional.of(StudentMapper.toTransfer(fromDb));
}
public void delete(int id) {
studentDao.delete(id);
}
}
The Controller and endpoint mappings
The controller is the main entry point, so we need to get the information from the HTTP request, map it into our internal data structure and call the service to do the business logic. In case anything goes wrong we return an HTTP status that correctly reflects what happened. I saw many services that always return 200, even if there was an error or the resource was not found, and provide the error only as a JSON response. This is incorrect and the status should reflect the actual state, with the JSON response needed only for additional information.
import com.fasterxml.jackson.databind.JsonNode;
import com.google.inject.Inject;
import data.dto.StudentDTO;
import play.libs.Json;
import play.mvc.Controller;
import play.mvc.Http;
import play.mvc.Result;
import services.StudentService;
import java.util.Optional;
public class StudentsController extends Controller {
@Inject
private StudentService studentService;
public Result get(Integer id) {
Optional<StudentDTO> student = studentService.getById(id);
if (student.isPresent()) {
return ok(Json.toJson(student.get()));
}
return notFound();
}
public Result getAll() {
return ok(Json.toJson(studentService.getAll()));
}
public Result create(Http.Request request) {
JsonNode jsonData = request.body().asJson();
try {
StudentDTO studentDTO = Json.fromJson(jsonData, StudentDTO.class);
studentDTO = studentService.create(studentDTO);
return ok(Json.toJson(studentDTO));
} catch (RuntimeException e) {
// Most probably invalid student data
return badRequest(request.body().asJson());
}
}
public Result update(Http.Request request, Integer id) {
JsonNode jsonData = request.body().asJson();
try {
StudentDTO studentDTO = Json.fromJson(jsonData, StudentDTO.class);
Optional<StudentDTO> student = studentService.update(studentDTO, id);
if (student.isPresent()) {
return ok(Json.toJson(student.get()));
}
return notFound();
} catch (RuntimeException e) {
return badRequest(request.body().asJson());
}
}
public Result delete(Integer id) {
studentService.delete(id);
return ok();
}
}
And now, we map our endpoints to the Controller methods.
GET /student controllers.StudentsController.getAll
GET /student/:id controllers.StudentsController.get(id: Integer)
+ nocsrf
POST /student controllers.StudentsController.create(request: Request)
+ nocsrf
PUT /student/:id controllers.StudentsController.update(request: Request, id: Integer)
+ nocsrf
DELETE /student/:id controllers.StudentsController.delete(id: Integer)
Some explanations
First thing you may notice is that the DAO uses jpaApi.withTransction(). This is to ensure we have a transaction when doing any write operation on the database. Otherwise, we won’t be able to do any changes and we will get an error: java.lang.IllegalArgumentException: Removing a detached instance.
Second, the fact that the controller catches a RuntimeException. This is because the JSON parser can throw this when an invalid JSON is provided. We will return a badRequest() with the provided input. A more advanced system may detect what is wrong and provide a more explicit message.
Last, but not least, the + nocsrf in the routing file. This is to allow the request even if no CSRF token is provided. Normally we do not allow this, but since this is a RESTful service that is not tied to an HTML, we allow it. For a production-ready service, we do need to add a way of authenticating and authorizing the requests, but that is a discussion for another article.
Article originally posted on my personal website under Building a REST API in Play Framework. Sample requests can also be found there
Posted on April 21, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.