đ Table of contents
- Sprint 2
- Sprint assesment
- Retrospective
- Is it done? â The Definition of Done
- đââď¸ Sprint 2 planning
- â Bonus: Reducing boilerplate code with Lombok
- REST APIs
- Designing the REST API endpoints for the user stories
- REST API documentation with Swagger
- Communication between frontend and backend
- Cross-Origin Resource Sharing (CORS)
- The .gitignore file
- Deploying the frontend
- Sprint Review
Sprint 2
For the Sprint 2 we have a new set of requirements from the Product Owner. On top of working on new user stories, we will be covering topics related to Retrospective, REST APIs and communication between frontend and backend.
Sprint assesment
This Sprint doesnât have a Moodle submission. It is enough that everything mentioned in the exercises is pushed to the projectâs GitHub repository before the Sprint deadline on 6.5. at 12:00. We will be working on the exercises for the next two weeks.
The Sprint assesment is done based on the exercises 1-35. The team can earn up to 15 points from this Sprint. The assesment is done at the end of the Sprint during the Sprint Review event.
Retrospective
The Scrum Guide only describes high-level guideliness for the software development process. It is not a one-size-fits-all framework suitable for every kind of project. Instead, the Scrum Team should adapt the process to match the project needs. To discuss about the issues with the process and to come up with solutions, the Scrum has a dedicated event called Retrospective.
After the Sprint Review, the Retrospective is the final event that ends the current Sprint. During the Retrospective, the Scrum Team openly discuss the issues with the process. Every member of the Scrum Team must be heard. Even more important than raising issues, is to find solutions together. These solutions are concrete actions that the Scrum Team can perform during the upcoming Sprint to improve the process. In the ideal case, after performing the necessary actions during the upcoming Sprint, the same issues arenât raised during the next Retrospective.
There are different Retrospective techniques to arrange the event. A quite common Retrospective technique is the Mad, Sad, Glad Retrospective. In the Mad, Sad, Glad Retrospective, each member of Scrum Team writes down things during the Sprint that made them feel either mad, sad or glad.
The things in the mad category are the ones that are very frustrating and are stopping you from performing at your best. For example:
- The Spring Boot application wonât start on my computer.
- I always have conflicts when I try to pull code from the GitHub.
The things in the sad category are the ones that are dissapointing, but you can cope with them. For example:
- The Daily Scrum meetings take too long.
- The Sprint Backlog isnât always up-to-date.
The things in the glad category are the ones that made you happy. For example:
- Communication was clear.
- Everybody contributed to the tasks.
Retrospectives can be organized face-to-face or using virtual platforms. During this course we will be using the Flinga platform to organize our Retrospectives. You can login to Flinga with your Haaga-Helia credentials by choosing âHaaga-Helia ammattikorkeakouluâ at the Flinga login page.
Once logged in a new session can be created by clicking the âCreate a sessionâ button. In the session you can send cards to the board by typing the message and clicking the âSendâ button. You can also choose a color for the card. Other team members can join the session using the âJoin linkâ which can be found by cliking the menu icon on the top left corner of the page.
First, create a session board that has the mad, sad and glad categories like in this board. Then, organize the Retrospective in the following manner:
- Based on their experiences during the Sprint, each team member should write cards for each of the three categories. Choose the card color based on the category. Try to come up with cards for each category. Donât discuss about the cards at this point because the cards should represent your personal opinions
- Once everyone is done writing the cards, go through the cards from each category. The writer of the card should shortly describe what their card means.
- Together, pick at least three successes during the Sprint from the glad category and three most urgent issues from the mad and sad categories. Come up with concrete actions to solve each of these issues during the upcoming Sprint. For example a concrete action for the issue âI always have conflicts when I try to pull code from the GitHubâ could be âPull code from GitHub more oftenâ and âCreate smaller commits and push code to GitHub more oftenâ.
Make sure that everyone follows the event structure and that each team member gets their voice heard.
Exercise 1
The Scrum Master should create a new session in Flinga as instructed above. Name the session âRetrospective 1â. Once the session is created, other team members should join the session with the âJoin linkâ. Setup the session board and organize the Retrospective event as instructed above.
Once you have completed the Retrospective, add
retrospectives
folder to the repository and in it asprint1.md
file. In that file, add a link to the Retrospectiveâs Flinga board and write down the issues and actions you came up with during the Retrospective and push the changes to GitHub. You can use this template in the file:# Sprint 1 Retrospective [Retrospective board](link-to-the-flinga-board) ## Successes - Something that we succeeded in. ## Issues - Some issue we had. ## Actions - Some action to fix an issue.
Exercise 2
Choose a new (not the same team member as during the previous Sprint) Scrum Master among the team members for the second Sprint.
Is it done? â The Definition of Done
âIs this task doneâ?
A common question among the team memberâs which is asked frequently for example during the Daily Scrum event. The answer depends on the team memberâs opinion on what âdoneâ means. The Definition of Done is a formal description of when a backlog item is considered done. It is a checklist of quality measures, which the Scrum Team writes down together. Every team member should commit to the Definition of Done and make sure that their work fulfills the common quality measures.
Exercise 3
Read the following article on the Definition of Done. Then, define your projectâs Definition of Done together and write it down in the
README.md
file under the âDefinition of Doneâ heading. You can consider for example the following quality measures in your Definition of Done:
- Should the task be deployed to the production environment?
- Should the task be manually tested both in development and in production environment?
- Should the relevant documentation be updated?
- Should other team member review the task-related code and implementation?
- Should the user story implementation be accepted by the Product Owner?
The quality measures of the Definition Done doesnât have to be too ambigious at start. Discuss as a team what are the most important quality measures for you. You can modify the Definition of Done as you go, just remember to communicate the changes with the whole team.
đââď¸ Sprint 2 planning
If you werenât able to implement all the user stories during the previous Sprint, start by finishing those before starting to implement the user stories for this Sprint.
The Product Owner was delighted to see how the project has advancend during Sprint 1.
The Sprint Review gave the Product Owner many new ideas on how to improve the application. Hereâs how the Product Owner is describing the Sprint 2 goals in the Sprint Planning event:
âItâs great that we now have the basic functionality for managing quizzes! What we now need is a way for the teacher to categorize quizzes and student to take the published quizzes.
To be able to categorize quizzes, the teacher should be able to add a category. A category has a name, for example âVocabularyâ and a description, for example âQuestions related to the vocabulary of a languageâ. The name or description shouldnât be blank and there shouldnât be multiple categories with the same name. There should be a form for adding a category and a separate page for listing the added categories. The categories should be listed in an alphabetical order based on the name.
The category list should have a delete button next to each category, which can be clicked to delete the category. There should also be a link which should take the teacher to an edit form, where they can edit all the information of the category.
The teacher should be able to choose the quizâs category while adding or editing a quiz. There could be a dropdown menu in both forms where categories are listed in an alphabetical order based on the name. The teacher should also be able leave the quiz uncategorized by not choosing a category.
Once we have this basic set of features for the teachers, we can start working on the student dashboard application. The student dashboard should have a page that lists the published quizzes with the quiz name, description and the date when it was added. These quizzes should be listed from newest to oldest.
Different students are interested in different quiz categories. To find interesting quizzes quickly, the student should be able to filter the quiz list based on the category. There could be dropdown menu at the top of the page for filtering the quizzes by a specific category.
Each quiz name on the quiz list should be a link to a separate page where the quiz name, description and the questions are displayed. There should also be some kind of navigation menu from which the student can navigate to the quiz list page.
In the quiz page the student should be able to take a published quiz by answering the questions. The questions should be listed and the student should be able to choose an answer option and submit their answer for each question. When the student submits their answer, there should be some kind of feedback which tells the student if their answer was correct or not. For example, âThat is correct, good job!â, or âThat is not correct, try againâ.
There should be a page in the student dashboard where the results of a quiz are displayed. The page should display the difficulty level, the total number of answers, the correct answer percentage and the number of correct and wrong answers for each question of the quiz. Also the total number of answers for the quiz should be displayed. There should be a link to the results page next to the quiz in the quiz list page.
Students have different skill levels so it would be useful if the student could filter the questions of quiz by the difficulty level in the quiz page. There could be dropdown menu at the top of the page from which the student can select the difficulty level for the questions.â
â The Product Owner
After some discussion the Scrum Team planned the following user stories:
- As a teacher, I want to add a category so that I can categorize quizzes
- As a teacher, I want to see a list of added categories so that I know which categories are added
- As a teacher, I want to choose a category for a quiz so that I can categorize quizzes
- As a teacher, I want to delete a category so that I can get rid of categories I donât need
- As a teacher, I want to edit a category so that I can change its information
- As a student, I want to see a list of published quizzes so that I know which quizzes I can take
- As a student, I want to see the questions of a quiz so that I know the contents of the quiz
- As a student, I want to answer the questions of a quiz so that I can know if my answers are correct
- As a user, I want to see the number of correct and wrong aswers of each question of a quiz so that I can know the results of a quiz
- As a student, I want to see a list of added categories so that I can browse quizzes in different categories
- As a student, I want to see a list of quizzes of a category so that I can browse the quizzes of a category
- As a student, I want to filter the questions of quiz by the difficulty level so that I can easily find suitable questions for my skill level
Exercise 3
Create a new milestone for the second Sprint. Set the milestone title as âSprint 2â.
Exercise 4
Make sure that all task related issues that have been completed during the Sprint 1 are closed and their status is âDoneâ in the Backlog project. Do the same with the user story related issues accepted by the Product Owner during the Sprint Review event.
If you didnât manage to implement all user stories during the previous Sprint, set the milestone of the unfinished user story and task issues as âSprint 2â. If the Sprint Review brought up implementation improvements or flaws (e.g. bugs), create appropriate issues for the tasks.
Exercise 5
Create an issue for each user story. Add the âuser storyâ label for each issue. Set the milestone as âSprint 2â. Add the issues to the Backlog project and move them to the âSprint Backlogâ column.
â Bonus exercise
To practice the feature branch workflow, implement some user stories or tasks in separate feature branches. Name the branch based on the feature, for example
add-category
orlist-quizzes-rest-api
. Create a separate branch for each feature (donât reuse the branch you created during the previous Sprint). For example:# first, make sure that we are on the main branch git checkout main git branch add-category git checkout add-category # time to start coding
Once the implementation in the branch is ready, open a pull request and let some other team member review it. Once the reviewer accepts the changes, merge the pull request to the main branch.
Exercise 6
Plan the tasks for the first user story, âAs a teacher, I want to add a category so that I can categorize quizzesâ. Read the Product Ownerâs Sprint Planning description regarding the user story again and split it into small coding tasks.
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
The Scrum Teamâs UI Designerâs vision is that the implementation could look something like this:
Exercise 7
Plan the tasks for the second user story, âAs a teacher, I want to see a list of added categories so that I know which categories are addedâ. Read the Product Ownerâs Sprint Planning description regarding the user story again and split it into small coding tasks.
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
The Scrum Teamâs UI Designerâs vision is that the implementation could look something like this:
Exercise 8
Plan the tasks for the third user story, âAs a teacher, I want to choose a category for a quiz so that I can categorize quizzesâ. Read the Product Ownerâs Sprint Planning description regarding the user story again and split it into small coding tasks.
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
The Scrum Teamâs UI Designerâs vision is that the implementation could look something like this:
Exercise 9
Plan the tasks for the fourth user story, âAs a teacher, I want to delete a category so that I can get rid of categories I donât needâ. Read the Product Ownerâs Sprint Planning description regarding the user story again and split it into small coding tasks.
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
Exercise 10
Plan the tasks for the fifth user story, âAs a teacher, I want to edit a category so that I can change its informationâ. Read the Product Ownerâs Sprint Planning description regarding the user story again and split it into small coding tasks.
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
Exercise 11
Write the first version of the projectâs data model documentation. Implement an entity relationship diagram and write a description of the applicationâs data model, which documents the applicationâs entities, their attributes, their relationships and the relationship types (one-to-one, one-to-many, or many-to-many). The description should explain the purpose of each entity and their relationship to other entities. Add the documentation under a âData modelâ subheading in the
README.md
file.GitHub supports Mermaid syntax for diagrams in Markdown files. Using Mermaid syntax makes it easier to maintain diagrams. Take a look at Mermaidâs Entity Relationship Diagrams documentation for more information.
NB: Keep this documentation (like all other documentation) up-to-date when you add new entities for the application.
â Bonus: Reducing boilerplate code with Lombok
Java classes require a lot of boilerplate code in the form of getters, setters and constructors. Each time we define an attribute for a class, we need to implement getter and setter methods for it and alter the constructor. Lombok is library that automatically generates getters, setters and constructors from the attributes.
Before we can start using Lombok, we need to add it as dependency for our Maven project. Letâs add the dependency to the <dependencies>
list in the pom.xml
file:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
Lombok also needs the IDE support. We can setup Lombok for Eclipse by following these instructions.
Once we have successfully setup the Lombok library, we can start using it in our classes. Lombok generates boilerplate code for classes using a set of annotations. For example, the @Getter annotation generates getters and the @Setter annotation setters for the class. We also need an empty constructor for Entity classes. We can use the @NoArgsConstructor annotation for this.
Hereâs an example of using these annotations on a Message
class:
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Message {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String content;
public Message(String content) {
this.content = content;
}
}
Wow, that eliminates a lot of boilerplate code! Lombok has many other useful annotations as well, such as the @Builder annotation. Check the documentation for more.
â Bonus exercise
Use Lombok annotations on classes to reduce boilerplate code in your project.
REST APIs
So far the user has interacted with our web application in the following manner:
- The user opens a page at certain path, for example
/
, in a web browser. - The web browser sends a request to the server (the backend) for the resource of that path.
- On the server, the request is delegated to the controller method responsible for handling requests for the path.
- The controller method retrieves the required data from the database and based on the data creates an HTML page.
- The HTML page is sent as a response and the browser displays the page for the user.
So, the representation of our applicationâs data is an HTML page. To provide other applications, such as JavaScript frontend applications, a better access to our data we need to represent it in a format that is easier to consume, such as the JSON format.
Instead of sending a response as an HTML page, we can serialize Java objects into text-based JSON format and send it as a response. Letâs consider a MessageRestController
controller class as an example:
@RestController
@RequestMapping("/api/messages")
public class MessageRestController {
@Autowired
private MessageRepository messageRepository;
@GetMapping("")
public List<Message> getAllMessages() {
return messageRepository.findAll();
}
}
The @RestController
annotation on the MessageRestController
class specifies that each method of the controller class produces a JSON response body. Instead of returning the name of the Thymeleaf template, we can directly return Java objects. For example the getAllMessages
method returns a list of Message
objects. If we open the page http://localhost:8080/api/messages in a web browser we should see this list.
By using JSON as the data representation format we can separate the client (the user interface application) from the server. This allows as to implement many different kinds of client applications with different programming languages. This separation of server and client is one of the corner stones of the the REST architectural style.
REST, or REpresentational State Transfer, is an architectural style for providing standards between computer systems on the web, making it easier for systems to communicate with each other. REST-compliant systems, often called RESTful systems, are characterized by how they are stateless and separate the concerns of client and server.
In a RESTful system, the requests must contain a path to a resource that the operation should be performed on. In RESTful APIs, paths should be designed to help the client know what is going on. For example, the path /users/29/messages
is a resource for messages of a user with a specific id. These paths are also referred to as endpoints.
The request should also contain the HTTP method, that determines the operation itself. The GET
method is used for endpoints that retrieve data, and never manipulate it. The POST
, PUT
and DELETE
methods on the other hand are used for endpoints that manipulate data. The POST
method is commonly used for endpoints that create database entries, the PUT
method for endpoints that update and the DELETE
method for endpoints that delete them.
The resource path has certain naming conventions. The path starts with the resource collection name in plural, for examples âusersâ. The collection name is followed by resource specifiers, for example the id of the resource. Hereâs example of RESTful API paths for the âusersâ collection:
HTTP method | Path | Request mapping | Description |
---|---|---|---|
GET | /users | @GetMapping("/users") | List all users |
GET | /users/{id} | @GetMapping("/users/{id}") | Get the user with the provided id |
POST | /users | @PostMapping("/users") | Create a user |
PUT | /users/{id} | @PutMapping("/users/{id}") | Update the user with the provided id |
DELETE | /users/{id} | @DeleteMapping("/users/{id}") | Delete the user with the provided id |
Collections are commonly entities which we are storing in the database (for example quizzes and questions). The REST API endpoints provide ways to access and manipulate these entities.
The {id}
part of the /users/{id}
path is a path variable. For example, the path for user with id 2 would be /users/2
.
The âidâ refers to the id attribute (the primary key) of the entity. The attributeâs name doesnât necessarily have to be âidâ.
A collection can have sub-collections. For example, a path for a userâs messages resource would be /users/{id}/messages
, where âmessagesâ is a sub-collection. This guide has more information about the resource path naming conventions.
When we design and implement REST API endpoints we should consider the use-case. We donât implement endpoints arbitrary, there should be a need for the endpoint first, for example a certain feature in a frontend application needs to display some data in the database. Based on the feature we consider what kind of data and operations the REST API needs to provide. These requirements will determine the endpoints we will implement.
We can create a separate controller class for each collection. The @RequestMapping
annotation can be applied to the controller class to define the collection name prefix of the path. Each method will automatically get the prefix in the path, so we donât need to repeat it in the methodâs request mapping annotations:
@RestController
@RequestMapping("/api/messages")
@CrossOrigin(origins = "*")
public class MessageRestController {
// ...
@GetMapping("/{id}")
public Message getMessageById(@PathVariable Long id) {
return messageRepository.findById(id).orElseThrow(
() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Message with id " + id + " does not exist"));
}
@PostMapping("")
public Message createMessage(@Valid @RequestBody CreateMessageDto message, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, bindingResult.getAllErrors().get(0).getDefaultMessage());
}
Message newMessage = new Message(message.getContent());
return messageRepository.save(newMessage);
}
}
Itâs handy to use some prefix, such as âapiâ to distinguish paths that produce JSON response from paths that produce HTML pages.
In this case, the getMessageById
method will handle GET request to the path /api/messages/{id}
and the createMessage
method will handle POST request to the path /api/messages
.
In the createMessage
method, the CreateMessageDto
object annotated with the @RequestBody
annotation contains the data in the request body. For example with JavaScript, the request body can be set with the fetch
function call in the following way:
fetch("http://localhost:8080/api/messages", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
// This is the data in the request body
body: JSON.stringify({ content: "Hello world!" }),
})
Controlling the request and response body format with DTO classes
To have full control over the format of the request and response body we can use DTO classes. Especially with the request body annotated by the @RequestBody
annotation, we should always use a DTO class object instead of an entity class object. This is because using an entity class object might accidently allow users to update undesired attributes of an entity as described here.
For example, in the createMessage
method the request body format is defined by the CreateMessageDto
class:
public class CreateMessageDto {
@NotBlank(message = "Content is required")
private String content;
// constructors, getters and setters
}
This corresponds to the following JSON format for the request body:
{ "content": "Hello world!" }
HTTP status codes and REST API error handling
Previously we have handled errors in requests by sending a redirect or rendering a Thymeleaf template with some error messages. With REST API endpoints we communicate errors with HTTP status codes and JSON formatted error objects containing details about the error. HTTP status codes are numeric codes that describe whether the request was successful or not.
Successful status codes are in range 200 - 299. Most common successful status code is 200 OK, which is a generic way to inform the client that the request succeeded. Client error status codes are in range 400 - 499. These status codes indicate that thereâs something wrong with the clientâs request. Most common client error status codes are:
- 404 Not Found: the server cannot find the requested resource
- 400 Bad Request: there was something wrong with the userâs request. For example the request body of a POST request is not valid
- 403 Forbidden: the user lacks the required authorization for the request. For example the user tries to update a resource that they donât have access to
- 401 Unauthorized: the user is not authenticated, meaning that the server canât identify the user. For example the user is not signed in and tries to access a resource which requires authentication
As in the previous example, we can use the ResponseStatusException exception class to send HTTP status code with the response in case of errors. The exception should be thrown with an appropriate HTTP status code and an error message. For example, the line:
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, bindingResult.getAllErrors().get(0).getDefaultMessage());
In the createMessage
method will send a 400 Bad Request
status code with the response in case the request body is not valid. The second parameter of the ResponseStatusException
constructor is the error message which will be displayed in the error response object:
{
"timestamp": "2023-11-20T07:55:10.918+00:00",
"status": 400,
"error": "Bad Request",
"trace": "...",
"message": "Content is required",
"path": "/api/messages"
}
If we donât throw a ResponseStatusException
exception, the 200 OK
status code will be sent with the response, which indicates the that request succeeded. To get more control over the response, we can use the ResponseEntity class:
@PostMapping("")
public ResponseEntity<?> createMessage(@Valid @RequestBody CreateMessageDto message, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
List<String> errorMessages = bindingResult.getAllErrors().stream().map((error) -> error.getDefaultMessage())
.collect(Collectors.toList());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMessages);
}
Message newMessage = new Message(message.getContent());
messageRepository.save(newMessage);
return ResponseEntity.status(HttpStatus.CREATED).body(newMessage);
}
The error messages wonât be visible by default. We can change this behavior by adding the following property to the application.properties
file:
server.error.include-message=always
Omitting attributes from the JSON response body
By default all the attributes are present in the JSON response body. We can omit attributes from the JSON response body by using the @JsonIgnore annotation on an attribute. We usually want to omit attributes that hold private information, for example userâs password hash, or attributes that can potentially contain lots of data, for example, the @OneToMany
annotated attributes. We can, for example omit the messages
attribute of a User
entity class in the following way:
@JsonIgnore
@OneToMany(mappedBy = "user")
private List<Message> messages;
To have more control over the attributes in the JSON response, DTO classes can be used instead of returning the entity classes directly from the controller methods.
Designing the REST API endpoints for the user stories
Next, letâs consider what kind of REST API endpoints we need for the last four user stories. Implement the following endpoints by following the REST API naming conventions for the endpoint path names.
Exercise 12
To classify frontend-related and backend-related issues, create two new labels: âfrontendâ and âbackendâ. Add the âfrontendâ label for issues that are related to the frontend implementation and the âbackendâ label for issues that are related to the backend implementation.
Omit the
@OneToMany
attributes from the JSON response in every entity by using the @JsonIgnore annotation on the attributes.
To keep the code organized, create a separate REST controller class for each REST API collection, such as âquizzesâ or âcategoriesâ. The collection-specfic path prefix can be added to all methods using the
@RequestMapping
annotation on the class.
GET method endpoints are easy to test with a web browser by just visiting the endpoint URL. POST method endpoints can be tested with tools such as Postman.
Exercise 13
Implement a REST API endpoint for getting all quizzes in newest to oldest order. Only published quizzes should be returned by the endpoint.
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
Exercise 14
Implement a REST API endpoint for getting a quiz by id. Return an appropriate HTTP status code and error message in the following error case:
- Quiz with the provided id does not exist
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
Exercise 15
Implement a REST API endpoint for getting the questions of a quiz. In this case, instead of using the
@JsonIgnore
annotation, you can use @JsonManagedReference and @JsonBackReference annotations to include the questionâs answer options in to the JSON response. Return an appropriate HTTP status code and error message in the following error case:
- Quiz with the provided id does not exist
Request parameters are used to provide additional properties with the request. A common use-case for a request parameter is filtering a collection based on some property, for example
/messages?content=Hello
.The endpoint should support an optional request parameter for filtering the questions of a quiz by a difficulty level. That is, if the request parameter is provided, only the questions with the provided difficulty level should be returned.
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
Exercise 16
Implement a REST API endpoint for creating an answer for a quizâs question. Before implementing the endpoint itself, consider what kind of data requirements the endpoint has. Return an appropriate HTTP status code and error message in the following error case:
- Answer option with the provided id does not exist
- Quiz is not published
- The request body is invalid, for example, the answer option is not provided
Define the request body format with a DTO class. The frontend can send a JSON request body for example in the following format:
{ "answerOptionId": 1 }
In this case, the following request body should be considered invalid and lead to a reponse with appropriate HTTP status code and error message:
{ "answerOptionId": null }
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
Exercise 17
Implement a REST API endpoint for getting the anwers of a quiz. Return an appropriate HTTP status code and error message in the following error case:
- Quiz with the provided id does not exist
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
Exercise 18
Implement a REST API endpoint for getting all categories in alphabetical order by the name.
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
Exercise 19
Implement a REST API endpoint for getting a category by id. Return an appropriate HTTP status code and error message in the following error case:
- Category with the provided id does not exist
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
Exercise 20
Implement a REST API endpoint for getting the quizzes of a category in newest to oldest order. Only published quizzes of the category should be returned by the endpoint. Return an appropriate HTTP status code and error message in the following error case:
- Category with the provided id does not exist
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
The GET method endpoint error responses will be displayed as a HTML error page in a web browser. To see what the JSON error response looks like, use Postman to send the request.
REST API documentation with Swagger
Now that we have implemented REST API endpoints for our application, we should describe to our fellow developers what these endpoints are and what kind of data they provide. We could write this documentation by hand, but thereâs a high risk that the documentation will become stale in the future. For example, if we make some alteration to the structure of the provided data and forget to update the documentation. That is why API documentation is commonly generated automatically by tools such as Spring Doc.
Spring Doc is a library for generating a JSON-formatted description of a REST API from our controller classes and their methods. This description follows a common format called the OpenAPI Specification format. Once the OpenAPI formatted description is generated, there are tools to display the information in a user-friendly way as a user interface. One of such tools is Swagger.
Swagger provides documentation for the API endpoints we define in the controller methods. The documentation is a user interface that lists the endpoints and provides information for each one, such as what the request for the endpoint looks like and whatâs in the response. We can also easily send requests and inspect the response using the user interface. Here is an example of a Swagger documentation.
Letâs start documenting our API by adding the Spring Doc dependency to the <dependencies>
list in the pom.xml
file:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
Then, letâs start our application and open http://localhost:8080/v3/api-docs in a browser. We should see the JSON formatted description of our REST API in the OpenAPI format. For a more user-friendly description, the Swagger documentation can be found at http://localhost:8080/swagger-ui/index.html. In the Swagger documentation, we can see a documentation for the REST controller classes.
We can find documentation for each REST API endpoint under the section named by the REST controller class. We see all relevant information about the endpoint: the path, path parameters, and an example of the response. We can send a test request by clicking the âTry it outâ button on the right. This is handy while we are exploring an API that we arenât familiar with.
We can provide more details about the endpoints by using specific annotations for the controller classes and methods. As an example, we could provide a better name and a description for the message API using the @Tag
annotation:
@RestController
@RequestMapping("/api/messages")
@CrossOrigin(origins = "*")
@Tag(name = "Message", description = "Operations for accessing and managing messages")
public class MessageRestController {
// ...
}
We can also provide more information about a specific endpoint using the @Operation
annotation:
@RestController
@RequestMapping("/api/messages")
@CrossOrigin(origins = "*")
@Tag(name = "Message", description = "Operations for accessing and managing messages")
public class MessageRestController {
// ...
@Operation(
summary = "Get a message by id",
description = "Returns the message with the provided id"
)
@GetMapping("/{id}")
public Message getMessageById(@PathVariable Long id) {
// ...
}
}
The @ApiResponses
and @ApiResponse
annotations can be used to document different kind of success and error responses and their corresponding HTTP status codes:
@Operation(
summary = "Get a message by id",
description = "Returns the message with the provided id"
)
@ApiResponses(value = {
// The responseCode property defines the HTTP status code of the response
@ApiResponse(responseCode = "200", description = "Successful operation"),
@ApiResponse(responseCode = "404", description = "Message with the provided id does not exist")
})
@GetMapping("/{id}")
public Message getMessageById(@PathVariable Long id) {
// ...
}
We cannot define more than one
@ApiResponse
annotation with the sameresponseCode
property (HTTP status code) inside a@ApiResonses
annotation.
Exercise 21
Generate a Swagger documentation for the project as described above. Add proper name and description for all REST controller classes using the
@Tag
annotation. For each REST controller method add a proper summary and description using the@Operation
annotation. Also add the@ApiResponses
annotation with an@ApiResponse
annotation for each success and error response.Test each REST API endpoint by opening the endpointâs documentation and cliking the âTry it outâ button. Remember to also test that the error responses work properly. For example send a request to the endpoint wich returns the questions of a quiz with an
id
path parameter value of a non-existing quiz.Add a link to the Swagger documentation in the production environment (in Render) under a âREST APIâ subheading in the
README.md
file. The link format is http://name-of-the-web-service.onrender.com/swagger-ui/index.html. Deploy the backend application to Render and make sure that the Swagger documentation is accessible. Also make sure that the error messages are visible in the JSON response by testing some REST API endpoint with an invalid request.
Communication between frontend and backend
With REST APIs we can separate the client application from the server application. In web applications these client applications are commonly called frontend applications.
The communication between the frontend application and the backend application is performed using the JavaScriptâs Fetch API. The Fetch API provides the fetch
function, which can be used to send a HTTP request to a specific URL:
fetch("http://localhost:8080/api/messages")
.then((response) => response.json())
.then((messages) => {
console.log(messages);
});
The default request method is GET. We can use a different request method, such as POST, by providing addional options for the fetch
call:
fetch("http://localhost:8080/api/messages", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ content: "Hello world!" }),
})
.then((response) => response.json())
.then((newMessage) => {
console.log(newMessage);
});
The
fetch
calls require somewhat boilerplate code, especially while sending JSON formatted data to the server with a POST request. Different HTTP client libraries such as Axios are used to reduce this boiplerate code and to provide useful additional features.
As an example, the logic of fetching and creating messages could be extracted into getAllMessages
and createMessage
functions:
const BACKEND_URL = "http://localhost:8080";
export function getAllMessages() {
return fetch(`${BACKEND_URL}/api/messages`).then((response) =>
response.json()
);
}
export function createMessage(message) {
return fetch(`${BACKEND_URL}/api/messages`, {
method: "post",
body: JSON.stringify(message),
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
}).then((response) => {
if (!response.ok) {
throw new Error("Failed to create the message");
}
return response.json();
});
}
These are simple abstractions for fetching and creating messages, but they are quite handy. If for example the logic for fetching the messages (for example the API URL) changes, we only need to change the logic inside the getAllMessages
function and nowhere else.
As an example, we could call the getAllMessages
function to display the messages list in a MessageList
component in the following way:
import { useEffect, useState } from "react";
import { getAllMessages, createMessage } from "../services/message";
export default function MessageList() {
const [messages, setMessages] = useState([]);
function fetchMessages() {
getAllMessages().then((messages) => {
setMessages(messages);
});
}
useEffect(() => {
fetchMessages();
}, []);
// ...
}
Exercise 22
Initialize a frontend application for the student dashboard application for example using Vite. You donât necessarily need a separate repository for the frontend application, you can initialize it in folder within the current repository.
Cross-Origin Resource Sharing (CORS)
The Vite development server is serving the JavaScript files from an URL that starts with http://localhost:5173
. Our backend is accessible in the URL http://localhost:8080
. When we send a request with the fetch
function from the frontend to our backend, we send a request to a different origin. These kind of requests are called cross-orgin requests.
Web browsers donât allow fetch
functions to send cross-origin requests by default. This is called the same-origin policy. We can however allow certain (or every) cross-origin request by using Cross-Origin Resource Sharing (CORS).
The idea of CORS is that the web browser âasksâ the backend if cross-origin request from a certain origin is allowed by sending a special HTTP request. If the backend allows the requests, then the web browser will send it. In a Spring Boot application we can use the @CrossOrigin annotation in the class or method level to allow cross-origin requests to certain or all paths of controller. For example we can allow cross-origin requests from all origins for all MessageRestController
method paths in the following way:
@RestController
@RequestMapping("/api/messages")
@CrossOrigin(origins = "*")
public class MessageRestController {
// ...
}
The remaining user stories are related to the student dashboard application and they should be implemented as a frontend application which uses REST API endpoints implemented in the backend.
Use the
@CrossOrigin
annnotation on the REST controller classes to allow cross-origin requests from the frontend application.
Exercise 23
Plan the tasks for the sixth user story, âAs a student, I want to see a list of published quizzes so that I know which quizzes I can takeâ. Read the Product Ownerâs Sprint Planning description regarding the user story again and split it into small coding tasks.
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
The Scrum Teamâs UI Designerâs vision is that the implementation could look something like this:
Exercise 24
Plan the tasks for the seventh user story, âAs a student, I want to see the questions of a quiz so that I know the contents of the quizâ. Read the Product Ownerâs Sprint Planning description regarding the user story again and split it into small coding tasks.
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
The Scrum Teamâs UI Designerâs vision is that the implementation could look something like this:
Exercise 25
Plan the tasks for the eighth user story, âAs a student, I want to answer the questions of a quiz so that I can know if my answers are correctâ. Read the Product Ownerâs Sprint Planning description regarding the user story again and split it into small coding tasks.
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
Tips for the tasks:
The Scrum Teamâs UI Designerâs vision is that the implementation could look something like this:
Exercise 26
Plan the tasks for the ninth user story, âAs a user, I want to see the number of correct and wrong aswers of each question of a quiz so that I can know the results of a quizâ. Read the Product Ownerâs Sprint Planning description regarding the user story again and split it into small coding tasks.
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
The Scrum Teamâs UI Designerâs vision is that the implementation could look something like this:
Exercise 27
Plan the tasks for the tenth user story, âAs a student, I want to see a list of added categories so that I can browse quizzes in different categoriesâ. Read the Product Ownerâs Sprint Planning description regarding the user story again and split it into small coding tasks.
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
The Scrum Teamâs UI Designerâs vision is that the implementation could look something like this:
Exercise 28
Plan the tasks for the eleventh user story, âAs a student, I want to see a list of quizzes of a category so that I can browse the quizzes of a categoryâ. Read the Product Ownerâs Sprint Planning description regarding the user story again and split it into small coding tasks.
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
The Scrum Teamâs UI Designerâs vision is that the implementation could look something like this:
Exercise 29
Plan the tasks for the twelfth user story, âAs a student, I want to filter the questions of quiz by the difficulty level so that I can easily find suitable questions for my skill levelâ. Read the Product Ownerâs Sprint Planning description regarding the user story again and split it into small coding tasks.
Create an issue for each task. Set the milestone as âSprint 2â. Add the issues to the Backlog projectâs âSprint Backlogâ column.
The Scrum Teamâs UI Designerâs vision is that the implementation could look something like this:
Exercise 30
Add instructions on how to start the frontend application to the âDeveloper guideâ section in the
README.md
file. Donât forget important details, such as in which folder the commands should be run in an how to install the frontend dependencies.For the sake of clarity, you can add separate subheadings for backendâs and frontendâs developer guide:
## Developer guide ### Backend The backend developer guide goes here. ### Frontend The frontend developer guide goes here.
You can test how good your user guide is by cloning a new copy of the repository and executing the steps precisely as they are in the developer guide without making any assumptions.
Exercise 31
Write the first version of the projectâs architecture documentation. Add the documentation under a âArchitectureâ subheading in the
README.md
file. The documentation should contain the following things:
- The projectâs overall architecture consists of three components: the backend, the database and the frontend. Mention these components and briefly explain the purpose of each component
- Implement a flow chart using the Mermaid syntax which visualizes how the components communicate with each other. Whatâs the direction of the communication (the arrow direction in the chart)? Does for example database send requests to the backend (the arrow would point to the backend) or the other way around?
- Which programming language, frameworks and major libraries are used in the backend implementation?
- Which database platforms are used in different environments (development and production environment)?
- Which programming language, frameworks and major libraries are used in the frontend implementation?
The .gitignore file
The .gitignore file specificies which files Git should ignore in a repository. This means that adding or changing ignored files wonât add the changes to the next commit while using the git add
command. There are a few reason why we want specific files or entire folders being ignored by Git and thus not end up in the remote repository:
- The file contains sensitive information, such as database usernames and passwords. We donât want these to end up in our repository for everyone to see.
- The file or folder contains content that can be generated by running a certain command. An example of such folder is the
node_modules
folder, which contains the library dependencies for the frontend. This folder is usally quite big and we can always generate it by runningnpm install
. Another such example is thetarget
folder where Maven generates different files. - The file or folder contains IDE specific configuration that is only relevant for a certain developer. An example of such folder is the
.vscode
folder.
As an example, the .gitignore
file generated by Vite has for example the following lines:
node_modules
dist
The ignored files (or in this case, the ignored folders) are specified by each line in the .gitignore
file. These two lines will ignore both the node_modules
and dist
folders.
Exercise 32
Make sure that your project has a
.gitignore
file which at least ignores the frontendâsnode_modules
folder, the frontendâs build folder (this isdist
folder in case of a Vite project) and the backendâstarget
folder. It is ok for the repository to have multiple.gitignore
files in different folders (for example having a different.gitignore
file for the frontendâs folder).Also check that the remote GitHub repository doesnât contain any execcessive files or folders (for example the mentioned
node_modules
andtarget
folders). Remove any execcessive files and folders and make necessary changes for the.gitignore
file if needed.
Deploying the frontend
We managed to deploy the backend during the previous Sprint, but we still havenât deployed the frontend. We can deploy the frontend to Render with the following steps:
-
In the frontend folder, add a
.env
environment variable file for the development environment. The.env
file should contain aVITE_BACKEND_URL
environment variable for the backendâs development environment URL:VITE_BACKEND_URL=http://localhost:8080
Make sure that every
fetch
function call has the environment variable as the URL prefix. For example:fetch(`${import.meta.env.VITE_BACKEND_URL}/api/messages`).then((response) => { // ... });
-
Add a
.env.production
environment variable file for the production environment. The.env.production
file should contain aVITE_BACKEND_URL
environment variable for the backendâs production environment URL. For example:VITE_BACKEND_URL=https://name-of-the-backend-service.onrender.com
Finally, push the changes to GitHub
- On the Render dashboard, click the âNewâ button and choose âStatic Siteâ
- From the repository list, find you projectâs repository and click the âConnectâ button
- Come up with the name for the service. If the frontend application is not initialized in the repositoryâs root folder (this is the case if you donât have a separate repository for the frontend application), set âRoot Directoryâ as the frontend folderâs name. Set âBuild Commandâ as
npm run build
and âPublish Directoryâ asdist
- Click the âAdvancedâ button and set âAuto-Deployâ as âYesâ and âBranchâ as âproductionâ
- Click the âCreate Static Siteâ button to create the service
- On the serviceâs page, click âRedirects/Rewritesâ from the navigation on the left. Set the âSourceâ as
/*
, âDestinationâ as/index.html
and âActionâ as âRewriteâ. Finally, click the âSave Changesâ button. This configuration will make the frontend routing work
Exercise 33
Deploy the frontend application to a production environment. Add the production environment URL of the frontend application (the web service URL in the Render dashboard) to the âDeveloper guideâ section in the
README.md
file.
Sprint Review
We have all kinds of cool stuff to show for the Product Owner at the end of this Sprint. Prepare for the upcoming Sprint Review event, similarly as in the previous Sprint.
Exercise 34
Once you have implemented the user stories of the Sprint and the main branch has a working version of the application, create a GitHub release for the project as instructed in the GitHubâs documentation. Create a new tag called âsprint2â. The release title should be âSprint 2â. Give a brief description for the release that describes the features implemented during the Sprint.
Exercise 35
The Scrum Master should prepare the Sprint Review demonstration at the beginning of the next Sprint. The Scrum Master should make sure that they have a working version of the application either deployed to Render (preferred) or on their computer and is able to show how the new features work in the userâs perspective. If possible, demonstrate the features in the production environment.
As in the previous Sprint Review, prepare some sensible test data for the Sprint Review.
Make sure that everything mentioned in the exercises is pushed to the projectâs GitHub repository before the Sprint 2 deadline on 6.5. at 12:00.