REST Basics (individual)
Here you will continue working on the Heroes API project you set up recently, but at first there are basics to cover. Please, read the explanations and follow the instructions after them.
Primer: Annotations
- Annotations are metadata attached to Java code (classes, methods, fields, parameters, etc.)
- They do not directly change program behavior by themselves; tools, frameworks, or the JVM can read them and act on them
- Examples of built-in annotations:
- @Override - indicates that a method overrides a superclass method
- @Deprecated - marks code that will be removed in a future version
- @SuppressWarnings - tells the JVM to ignore certain warnings
- Annotations can have parameters (e.g., @RequestMapping(path = "/heroes", method = RequestMethod.GET))
- Various frameworks and libraries bring their own annotations, and a developer can create custom ones
Primer: Spring Annotations
- Spring Framework (a framework under the hood of Spring Boot) provides multiple annotations, there are some examples
- Class annotations:
- @Component - instantiates a single instance of the class and injects it into other classes when needed
- @RestController - identifies a class as a HTTP request handler
- @RequestMapping - maps the URL pattern to the class that handles corresponding requests
- Method annotations:
- @GetMapping - maps the URL pattern to the method that handles GET requests
- @PostMapping - maps the URL pattern to the method that handles POST requests
- @PutMapping - maps the URL pattern to the method that handles PUT requests
- @DeleteMapping - maps the URL pattern to the method that handles DELETE requests
- Parameter annotations:
- @PathVariable - binds a method parameter to a URI variable from the Maping annotation
- @RequestParam - binds a method parameter to a query parameter in the HTTP request
- @RequestBody - binds a method parameter to the body of the HTTP request
- @RequestHeader - binds a method parameter to a request header
- @Value - binds a method parameter with a value from the application.properties / application.yml file
Primer: Serialization, Deserialization, Jackson
- Serialization is the process of converting an object into a format that can be easily stored or transmitted (e.g., JSON, XML)
- Deserialization is the reverse process of converting serialized data back into an object
- Jackson is a popular Java library for handling JSON serialization and deserialization
- Spring Boot uses Jackson by default to convert Java objects to JSON and vice versa
- In Hero API, and your term project's API, there are at least 2 places where serialization and deserialization will be needed
- Handling of HTTP requests and responses
- HTTP Request JSON objects from the frontend will be deserialized into Java objects
- REST API Java objects will be serialized into HTTP Response JSON objects
- Spring Framework will do this automatically when we use the @RequestBody annotation in Controller classes
- Persisting data to and from files
- Java objects will be serialized into JSON objects and written as text to files
- JSON objects as text will be read from files and deserialized into Java objects
- ObjectMapper class from the Jackson library provides functionality for this
- Handling of HTTP requests and responses
Hero API Project Structure
There are 5 packages:
- com.heroes.api.heroesapi.controller - responsible for handling HTTP requests and responses
- com.heroes.api.heroesapi.entity - contains the data models for the application
- com.heroes.api.heroesapi.interceptor - contains the logic that is executed before / after each HTTP request is processed
- com.heroes.api.heroesapi.repository - contains the data access layer for the application
- com.heroes.api.heroesapi.service - contains the business logic for the application
Entities / Models
- There is a nuanced difference between entities and models, but in this class, we will use these names interchangeably
- In general, entities / models are core data structures that represent the business domain or just data in an application
- In Hero API, we have Hero class in the com.heroes.api.heroesapi.entity package representing a Hero
- Hero class contains id and name properties, and getters, setters, a constructor, and other standard methods
- As we accept Hero objects in our HTTP requests and return them in responses (and as we also persist data to and from files), we use Jackson annotations on the Hero class properties to control the serialization and deserialization process:
- @JsonProperty("id") private int id;
- @JsonProperty("name") private String name;
- @JsonProperty - tells Jackson to map a class property to the JSON representation using a specific name
- If you replace @JsonProperty("id") with @JsonProperty("qwe"), the resulting JSON will look like { "qwe": 1, "name": "Superman" } instead of { "id": 1, "name": "Superman" }
- If you do not put the @JsonProperty annotation on a property, Jackson will use the property name as the key in the JSON representation - it makes the code less bloated, but also makes it more fragile as any property name changes will break the JSON structure
- Take a look at the Hero class in the com.heroes.api.heroesapi.entity
Repositories
- There is also a nuanced difference between repositories and DAOs (Data Access Objects), but in practice, people mostly use these terms interchangeably (in Spring Framework, we traditionally use the term "repository")
- Repositories are used to localize data access logic and provide a clean interface to the rest of the application
- As all the data access logic is centralized in repositories, it
- makes the code easier to maintain and test
- allows for easier switching between different data sources (e.g., from a file-based storage to a database)
- In Hero API, we have a HeroRepository interface and a HeroFileRepository file-based implementation in the com.heroes.api.heroesapi.repository package
- If later we decide to switch to a database, we can create a HeroDBRepository implementation and update the application to use it without changing the rest of the code - the interface will remain the same
- Our storage mechanism will be a file data/heroes.json storing JSON objects representing a collection of Hero objects
- Take a look at the HeroRepository interface and the HeroFileRepository implementation in com.heroes.api.heroesapi.repository, and carefully read the comments and the code to understand how it works
Controllers
- Controllers are responsible for handling HTTP requests and responses, including request validation, nothing more - the rest of the functionality should be delegated to other components (e.g., services, repositories)
- REST API assumes one controller per resource, e.g., if we built a university management system, we would have
- StudentController - handles requests to /students
- CourseController - handles requests to /courses
- ProfessorController - handles requests to /professors
- etc
- Each controller method is responsible for one type of requests (GET, POST, etc) for a specific resource (e.g., GET /students)
- E.g., if we execute a GET-request to https://api.university.edu/students/34, Spring will route it to StudentController as the class is annotated with @RequestMapping("/students"), and then further to the method get(@PathVariable("id") long id) inside of the controller as the method is annotated with @GetMapping("/{id}") (we use @PathVariable("id") to bind 34 from the URL to the method parameter via /{id} in @GetMapping)
- Another example: if we execute a GET-request to https://api.university.edu/students, Spring will route it to StudentController as the class is annotated with @RequestMapping("/students"), and then further to the method getAll() inside of the controller as the method is annotated with @GetMapping (we don't need to specify the path since it's empty after /students)
- ResponseEntity in Spring is used to represent the entire HTTP response, including the status code, headers, and body, and should be returned from any controller method processing a request.
- Take a look at the HeroController class in the com.heroes.api.heroesapi.controller package, and carefully read the comments and the code to understand how it works.
Interceptors
- Interceptors are used to intercept and process HTTP requests and responses before they reach the controller or after they leave the controller correspondingly
- They can be used for various purposes, such as logging, authentication, authorization, and modifying requests or responses
- In Hero API, we have a LogInterceptor class in the com.heroes.api.heroesapi.interceptor package that logs incoming requests and outgoing responses to avoid duplicating the logging logic in the controller methods
- Take a look at the LogInterceptor class in the com.heroes.api.heroesapi.interceptor package, and carefully read the comments and the code to understand how it works
Services
- Services are responsible for orchestrating the flow of data between the controller and the repository, and it may also contain additional business rules or validation logic
- Controllers are responsible for request format validation, but some validation rules are about business logic, not just a format. E.g., if we get a request to create a new hero, the controller will check that the hero's name in the request is not empty (the request is formally valid), but the service will check if the hero's name is unique in the system (the request can be processed without breaking the system's internal integrity)
- Take a look at the HeroService class in the com.heroes.api.heroesapi.service package, and carefully read the comments and the code to understand how it works
cURL
GET-requests can be easily tested in a browser as you did at the beginning of the assignment, but any other HTTP methods (POST, PUT, DELETE, etc.) are not supported by a browser address bar. To test these methods, you can use cURL - a command-line tool that exists in any modern operating system. To execute the same request with cURL, you can use the following command.
For Windows (PowerShell),
- curl.exe -X GET http://localhost:8080/heroes/11
For Windows (Command Prompt), MacOS and Linux,
- curl -X GET http://localhost:8080/heroes/11
By default, cURL will show only a response body. If you want to see a full response, including a HTTP response code and headers, you can use the -i flag. E.g.,
- curl -i -X GET http://localhost:8080/heroes/11
- curl -v -X GET http://localhost:8080/heroes/11
The general cURL syntax looks like this: curl [OPTIONS] -X [METHOD] [URL] -H [HEADER] -d [BODY]. If you need to include several headers, just add more -H [HEADER] flags. E.g.,
For Windows (PowerShell),
- curl.exe -v -X POST http://localhost:8080/heroes -H "Content-Type: application/json" -d '{\"name\": \"Superman\", \"power\": \"Flight\"}'
For Windows (Command Prompt),
- curl -v -X POST http://localhost:8080/heroes -H "Content-Type: application/json" -d "{\"name\": \"Superman\", \"power\": \"Flight\"}"
For MacOS and Linux,
- curl -v -X POST http://localhost:8080/heroes -H "Content-Type: application/json" -d '{"name": "Superman", "power": "Flight"}'
When you execute these commands, you should receive 501 (Not Implemented) as a HTTP response code as hero creation endpoint is not implemented properly yet.
While it's important to know how to use cURL (you will meet a lot of situations where it's the only available tool), for the sake of convenience, you can also use tools like Postman or Insomnia to test your REST API endpoints.
Assignment
Implement the following functionality in the Hero API project. All the methods are created for you, but you need to implement logic for many of them. Note, that you will need to implement logic for HeroController and in some cases for HeroService.
| Purpose | HTTP Method | Endpoint | Headers | Request Body | Response |
|---|---|---|---|---|---|
| Create a hero | POST | /heroes | Content-Type: application/json | { "name": "Bolt" } | Create and return a hero object with a status of CREATED |
| Update a hero | PUT | /heroes/{id} | Content-Type: application/json | { "name": "Zoom" } | If a hero with id exists, update the name and return the updated hero object with a status of OK Otherwise, return a status of NOT FOUND |
| Delete a hero | DELETE | /heroes/{id} | n/a | n/a | If a hero with id exists, delete the hero and return a status of OK Otherwise, return a status of NOT FOUND |
| Get all heroes | GET | /heroes | n/a | n/a | Return a list of all heroes with a status of OK |
| Search for heroes | GET | /heroes?name={text} | n/a | n/a | Return a list of heroes whose name contains text (may be empty) and a status of OK |
Example Output
Here are some example of expected requests and responses for the Hero API:
Request: curl -X GET http://localhost:8080/heroes/11
Response: {"id":11,"name":"Mr. Nice"}
Request: curl -X GET http://localhost:8080/heroes
Response: [{"id":11,"name":"Mr.
Nice"},{"id":12,"name":"Narco"},{"id":13,"name":"Bombasto"},{"id":14,"name":"Celeritas"},{"id":15,"name":"Magneta" },{"id":16,"name":"RubberMan"},{"id":17,"name":"Dynama"},{"id":18,"name":"Dr
IQ"},{"id":19,"name":"Magma"},{"id":20,"name":"Tornado"},{"id":22,"name":"Mr Wonderful"}]
Request: curl -X GET http://localhost:8080/heroes?name=ag
Response: [{"id":15,"name":"Magneta"},{"id":19,"name":"Magma"}]
Request: curl -X POST http://localhost:8080/heroes -H 'Content-Type:application/json' -d '{"name": "Mr Wonderful"}'
Response: {"id":23,"name":"Mr Wonderful"}
Note that it's not the whole list of possible requests and responses. To understand the expected behaviour, look at
- the table above
- the HeroController class in the com.heroes.api.heroesapi.controller package, its methods and their documentation
- the HeroService class in the com.heroes.api.heroesapi.service package, its methods and their documentation
To check that your implementation behaves as expected, you can execute mvn clean test in the project directory to run the unit tests. If all tests pass, your implementation is correct.
Submission
In the root directory of the project, execute mvn exec:exec@zip to create a zip file of your project target/heroes-api.zip. Submit the zip file to the assignment submission page on myCourses.