1. Overview
The purpose of this tutorial is to explore the Play Framework and learn how to build REST services with it using Java.
We’ll put together a REST API to create, retrieve, update, and delete student records.
In such applications, we would normally have a database to store student records. The Play Framework has a built-in H2 database, along with support for JPA with Hibernate and other persistence frameworks.
However, to keep things simple and focus on the most important stuff, we will use a simple map to store student objects with unique IDs.
2. Create a New Application
Once we’ve installed the Play Framework as described in our Introduction to the Play Framework, we’re ready to create our application.
Let’s use the sbt command to create a new application called student-api using play-java-seed:
sbt new playframework/play-java-seed.g8
3. Models
With our application scaffolding in place, let’s navigate to student-api/app/models and create a Java bean for handling student information:
public class Student {
private String firstName;
private String lastName;
private int age;
private int id;
// standard constructors, getters and setters
}
We’ll now create a simple data store – backed by a HashMap – for student data, with helper methods to perform CRUD operations:
public class StudentStore {
private Map<Integer, Student> students = new HashMap<>();
public Optional<Student> addStudent(Student student) {
int id = students.size();
student.setId(id);
students.put(id, student);
return Optional.ofNullable(student);
}
public Optional<Student> getStudent(int id) {
return Optional.ofNullable(students.get(id));
}
public Set<Student> getAllStudents() {
return new HashSet<>(students.values());
}
public Optional<Student> updateStudent(Student student) {
int id = student.getId();
if (students.containsKey(id)) {
students.put(id, student);
return Optional.ofNullable(student);
}
return null;
}
public boolean deleteStudent(int id) {
return students.remove(id) != null;
}
}
4. Controllers
Let’s head over to student-api/app/controllers and create a new controller called StudentController.java. We’ll step through the code incrementally.
First off, we need to configure an HttpExecutionContext. We’ll implement our actions using asynchronous, non-blocking code. This means that our action methods will return CompletionStage
There is just one caveat when dealing with asynchronous programming in a Play Framework controller: we have to provide an HttpExecutionContext. If we don’t supply the HTTP execution context, we’ll get the infamous error “There is no HTTP Context available from here” when calling the action method.
Let’s inject it:
private HttpExecutionContext ec;
private StudentStore studentStore;
@Inject
public StudentController(HttpExecutionContext ec, StudentStore studentStore) {
this.studentStore = studentStore;
this.ec = ec;
}
Notice we have also added the StudentStore and injected both fields in the constructor of the controller using the @Inject annotation. Having done this, we can now proceed with the implementation of the action methods.
Note that Play ships with Jackson to allow for data processing – so we can import any Jackson classes we need without external dependencies.
Let’s define a utility class to perform repetitive operations. In this case, building HTTP responses.
So let’s create student-api/app/utils package and add Util.java in it:
public class Util {
public static ObjectNode createResponse(Object response, boolean ok) {
ObjectNode result = Json.newObject();
result.put("isSuccessful", ok);
if (response instanceof String) {
result.put("body", (String) response);
} else {
result.putPOJO("body", response);
}
return result;
}
}
With this method, we’ll be creating standard JSON responses with a boolean isSuccessful key and the response body.
We can now step through the actions of the controller class.
4.1. The create Action
Mapped as a POST action, this method handles the creation of the Student object:
public CompletionStage<Result> create(Http.Request request) {
JsonNode json = request.body().asJson();
return supplyAsync(() -> {
if (json == null) {
return badRequest(Util.createResponse("Expecting Json data", false));
}
Optional<Student> studentOptional = studentStore.addStudent(Json.fromJson(json, Student.class));
return studentOptional.map(student -> {
JsonNode jsonObject = Json.toJson(student);
return created(Util.createResponse(jsonObject, true));
}).orElse(internalServerError(Util.createResponse("Could not create data.", false)));
}, ec.current());
}
We use a call from the injected Http.Request class to get the request body into Jackson’s JsonNode class. Notice how we use the utility method to create a response if the body is null.
We’re also returning a CompletionStage
We can pass to it any String or a JsonNode, along with a boolean flag to indicate status.
Notice also how we use Json.fromJson() to convert the incoming JSON object into a Student object and back to JSON for the response.
Finally, instead of ok() which we are used to, we are using the created helper method from the play.mvc.results package. The idea is to use a method that gives the correct HTTP status for the action being performed within a particular context. For example, ok() for HTTP OK 200 status, and created() when HTTP CREATED 201 is the result status as used above. This concept will come up throughout the rest of the actions.
4.2. The update Action
A PUT request to http://localhost:9000/ hits the *StudentController.*update method, which updates the student information by calling the updateStudent method of the StudentStore:
public CompletionStage<Result> update(Http.Request request) {
JsonNode json = request.body().asJson();
return supplyAsync(() -> {
if (json == null) {
return badRequest(Util.createResponse("Expecting Json data", false));
}
Optional<Student> studentOptional = studentStore.updateStudent(Json.fromJson(json, Student.class));
return studentOptional.map(student -> {
if (student == null) {
return notFound(Util.createResponse("Student not found", false));
}
JsonNode jsonObject = Json.toJson(student);
return ok(Util.createResponse(jsonObject, true));
}).orElse(internalServerError(Util.createResponse("Could not create data.", false)));
}, ec.current());
}
4.3. The retrieve Action
To retrieve a student, we pass in the id of the student as a path parameter in a GET request to http://localhost:9000/:id. This will hit the retrieve action:
public CompletionStage<Result> retrieve(int id) {
return supplyAsync(() -> {
final Optional<Student> studentOptional = studentStore.getStudent(id);
return studentOptional.map(student -> {
JsonNode jsonObjects = Json.toJson(student);
return ok(Util.createResponse(jsonObjects, true));
}).orElse(notFound(Util.createResponse("Student with id:" + id + " not found", false)));
}, ec.current());
}
4.4. The delete Action
The delete action is mapped to http://localhost:9000/:id. We supply the id to identify which record to delete:
public CompletionStage<Result> delete(int id) {
return supplyAsync(() -> {
boolean status = studentStore.deleteStudent(id);
if (!status) {
return notFound(Util.createResponse("Student with id:" + id + " not found", false));
}
return ok(Util.createResponse("Student with id:" + id + " deleted", true));
}, ec.current());
}
4.5. The listStudents Action
Finally, the listStudents action returns a list of all the students that have been stored so far. It’s mapped to http://localhost:9000/ as a GET request:
public CompletionStage<Result> listStudents() {
return supplyAsync(() -> {
Set<Student> result = studentStore.getAllStudents();
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonData = mapper.convertValue(result, JsonNode.class);
return ok(Util.createResponse(jsonData, true));
}, ec.current());
}
5. Mappings
Having set up our controller actions, we can now map them by opening the file student-api/conf/routes and adding these routes:
GET / controllers.StudentController.listStudents()
GET /:id controllers.StudentController.retrieve(id:Int)
POST / controllers.StudentController.create(request: Request)
PUT / controllers.StudentController.update(request: Request)
DELETE /:id controllers.StudentController.delete(id:Int)
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
The /assets endpoint must always be present for downloading static resources.
After this, we’re done with building the Student API.
To learn more about defining route mappings, visit our Routing in Play Applications tutorial.
6. Testing
We can now run tests on our API by sending requests to http://localhost:9000/ and adding the appropriate context. Running the base path from the browser should output:
{
"isSuccessful":true,
"body":[]
}
As we can see, the body is empty since we haven’t added any records yet. Using curl, let’s run some tests (alternatively, we can use a REST client like Postman).
Let’s open up a terminal window and execute the curl command to add a student:
curl -X POST -H "Content-Type: application/json" \
-d '{"firstName":"John","lastName":"Baeldung","age": 18}' \
http://localhost:9000/
This will return the newly created student:
{
"isSuccessful":true,
"body":{
"firstName":"John",
"lastName":"Baeldung",
"age":18,
"id":0
}
}
After running the above test, loading http://localhost:9000 from the browser should now give us:
{
"isSuccessful":true,
"body":[
{
"firstName":"John",
"lastName":"Baeldung",
"age":18,
"id":0
}
]
}
The id attribute will be incremented for every new record we add.
To delete a record we send a DELETE request:
curl -X DELETE http://localhost:9000/0
{
"isSuccessful":true,
"body":"Student with id:0 deleted"
}
In the above test, we delete the record created in the first test, now let’s create it again so that we can test the update method:
curl -X POST -H "Content-Type: application/json" \
-d '{"firstName":"John","lastName":"Baeldung","age": 18}' \
http://localhost:9000/
{
"isSuccessful":true,
"body":{
"firstName":"John",
"lastName":"Baeldung",
"age":18,
"id":0
}
}
Let’s now update the record by setting the first name to “Andrew” and age to 30:
curl -X PUT -H "Content-Type: application/json" \
-d '{"firstName":"Andrew","lastName":"Baeldung","age": 30,"id":0}' \
http://localhost:9000/
{
"isSuccessful":true,
"body":{
"firstName":"Andrew",
"lastName":"Baeldung",
"age":30,
"id":0
}
}
The above test demonstrates the change in the value of the firstName and age fields after updating the record.
Let’s create some extra dummy records, we’ll add two: John Doe and Sam Baeldung:
curl -X POST -H "Content-Type: application/json" \
-d '{"firstName":"John","lastName":"Doe","age": 18}' \
http://localhost:9000/
curl -X POST -H "Content-Type: application/json" \
-d '{"firstName":"Sam","lastName":"Baeldung","age": 25}' \
http://localhost:9000/
Now, let’s get all the records:
curl -X GET http://localhost:9000/
{
"isSuccessful":true,
"body":[
{
"firstName":"Andrew",
"lastName":"Baeldung",
"age":30,
"id":0
},
{
"firstName":"John",
"lastName":"Doe",
"age":18,
"id":1
},
{
"firstName":"Sam",
"lastName":"Baeldung",
"age":25,
"id":2
}
]
}
With the above test, we are ascertaining the proper functioning of the listStudents controller action.
7. Conclusion
In this article, we’ve shown how to build a full-fledged REST API using the Play Framework.
As usual, the source code for this tutorial is available over on GitHub.