Skip to content

2. How does it work?

Sille Kamoen edited this page May 11, 2017 · 1 revision

The API is mostly based around the Model-View-Controller design pattern. Spring adds some layers of abstraction in between. This section will explain the main flow of information. The API has a RESTful design, which basically means all operations are done using standard GET, POST, PUT, PATCH and DELETE requests.

This page describes everything that's involved when a user registers for our website. When the user presses "Submit" on the registration form in the front-end, a request is sent to the API.

The application only accepts requests on url's we define ourselves. A valid URL is matched to a method using a "mapping". Everything we want to accept as a request has to be defined in a Controller. If there's a controller that registered a request, Spring forwards the request to that Controller. The controller that's responsible for our User registration is the UserRestController, located here.

Let's take a closer look

1 @RestController
2 @RequestMapping("/users")
3 public class UserRestController {
4 
5     private final UserService userService;
6 
7     private final SeatService seatService;
8 
9     @Autowired
10    UserRestController(UserService userService, SeatService seatService) {
11        this.userService = userService;
12        this.seatService = seatService;
13    }

Line 1 marks this class as a RestController. Spring will scan this class for mappings and registers them. Line 2 defines the mapping for this Controller. All mappings defined in this controller will be relative to the controller mapping. We'll come back to this later. This Controller doesn't perform any logic, that's delegated to a Service. Those services only have to be instatiated once, which we leave up to Spring. The @Autowired annotation takes care of binding the right service instances, similar to a Singleton design pattern. This is Spring magic, fully understanding how this works is out of scope.

Let's find the method responsible for user registration.

14    @RequestMapping(method = RequestMethod.POST)
15    public ResponseEntity<?> add(HttpServletRequest request, @Validated @RequestBody UserDTO input) {
          (...)
25    }

As you can see, line 14 contains another @RequestMapping annotation. In this case, no value is assigned, but as the class is mapped to "/users", this mapping inherits that. This means that a POST request to "/users" is handled by this Controller method.

Moving on to the method signature in line 15. The return object ResponseEntity<?> represents the JSON object we're returning to the frontend. The ?means it can contain an object that will be converted to JSON. More on this later. This method is named add, but this doesn't really matter as the RequestMapping takes care of the calling. The method has 2 parameters. Spring allows for some binding of these parameters, somewhat similar to the @Autowired annotation. The first parameter is the "raw" http request, which we need later on to put the correct URL in the registration mail. The second parameter is a lot more common. It's annotated by @Validated and @RequestBody. @Validated means that the constraints present in the UserDTO class are enforced and thrown an exception when violated. @RequestBody means that the requestbody will be mapped on the UserDTO class. We will now go into more detail on this UserDTO class.

The UserDTO stands for Data Transfer Object. It's a Java representation of the incoming data. In our case, the front-end sends a JSON requestbody. All the fields in this JSON are also present in the DTO, allowing easy handling in our Java classes.

1 public class UserDTO {
2 
3     @NotEmpty
4     @Getter
5     private String username = "";
6 
7     @NotEmpty
8     @Getter
9     private String password = "";
10
11    @Getter
12    private Long orderId;
}

It's a very simple class containing 3 annotated fields. The @Getter annotation is a Lombok annotation, which will automatically generates a getter for this field. The two @NotEmpty annotations are constraints, meaning this field can not be empty. Because we have a @Validated annotation in our Controller, an empty field here will cause an exception. The orderId field is present here in case the User registers after creating an Order, so we can link the Order to the newly registered User.

Back to our method

14    @RequestMapping(method = RequestMethod.POST)
15    public ResponseEntity<?> add(HttpServletRequest request, @Validated @RequestBody UserDTO input) {
16        User save = userService.create(input, request);
17
18        // Create headers to set the location of the created User object.
19        HttpHeaders httpHeaders = new HttpHeaders();
20        httpHeaders.setLocation(
21                ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(save.getId()).toUri());
22
23        return createResponseEntity(HttpStatus.CREATED, httpHeaders,
24                "User successfully created at " + httpHeaders.getLocation(), save);
25    }

The first line of our method (16) is a call to the UserService. We use Services for all our logic. The Controller is responsible for:

  1. Mapping an endpoint
  2. Receiving requests on that endpoint
  3. Authorizing the request (check if the current User is allowed to execute the method)
  4. Calling the right services
  5. Returning the response to the User

As stated, we have Services to do all of our "business logic". This way, we can re-use these methods anywhere in an organized way. Services consists of an interface (the UserService in this case is an interface) and an implementation of this interface (UserServiceImpl.java). You can take a look at the interface here, but it's pretty straightforward.

More intersting is the implementation, which can be found in UserServiceImpl.java.

Let's take a closer look. First, the first few lines.

1  @Service
2  public class UserServiceImpl implements UserService, UserDetailsService {
3  
4      private final UserRepository userRepository;
5      private final VerificationTokenRepository verificationTokenRepository;
6      private final PasswordResetTokenRepository passwordResetTokenRepository;
7      private final MailService mailService;
8  
9      @Value("${a5l.mail.confirmUrl}")
10     String requestUrl;
11     @Value("${a5l.user.resetUrl}")
12     String resetUrl;
13     @Value("${a51.user.alcoholage : 18}")
14     Long ALCOHOL_AGE;
15
16     @Autowired
17     public UserServiceImpl(UserRepository userRepository, VerificationTokenRepository verificationTokenRepository,
18                            PasswordResetTokenRepository passwordResetTokenRepository, MailService mailService) {
19         this.userRepository = userRepository;
20         this.verificationTokenRepository = verificationTokenRepository;
21         this.mailService = mailService;
22         this.passwordResetTokenRepository = passwordResetTokenRepository;
23    }

We start with the @Service annotation, which is a Spring "Stereotype". It tells Spring that this class is a Service class that is available for autowiring. By adding that annotation, Spring is able to automatically use this implementation to our UserService in the UserController, which we only declared as the interface! In theory, you could tell Spring to use another implementation of this interface, but that's uncommon on our application. Key thing to remember here is, if you create a new Service, make an interface, then the implementation and annotate it with @Service.

As you can see we're also implementing the UserDetailsService. This is a Spring service used for Spring Security which we'll discuss later. It defines a few methods to get Users from the database. Speaking of the database, the first declared field you see is the UserRepository that's a piece of proper Spring magic that nobody really understands but is extremely awesome nevertheless.

Our connection with the database (PostgreSQL) is done through so called JPA Repositories. This is a Java interface of which we use the Hibernate implementation. Not very interesting, but let's take a look at this magic. Behold, our full User database read/write class:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findOneByUsernameIgnoreCase(String username);

    Optional<User> findOneByProfileDisplayNameIgnoreCase(String displayName);
}

That's it. Every database operation can be done using this interface. We have to indicate which Java object we want to store, and which type the id field is, and the interface handles the rest. Okay, it's not that simple, as the object we want to store needs some annotations for this to work. You can take a look at the User object here. It's actually one of our most complicated models so don't worry too much about it. You can take a look at the Team class here for a more common class. Queries are automatically derived from the method name, which is pretty intuitive and more flexible than you might think. The best thing is, the interface is independent from the actual database. If we decide to switch to some epic new database, we don't have to rewrite any code.

Anyway, back to the relevant part of the Service. Before we go to the create method we called from the Controller, let's take a quick look at the fields annotated with @Value like

13 @Value("${a51.user.alcoholage : 18}")
14 Long ALCOHOL_AGE;

This annotation assigns a value from the application.properties, which is the way we configure some details for our applications. This includes stuff like database connection information and other things which are specific to our usecase. We don't (or at least try to) not hardcode any Area FiftyLAN specific values, as this API can be used for any event like ours.

To get back on track, here's the create method we came here to find.

24     @Override
25     public User create(UserDTO userDTO, HttpServletRequest request) throws DataIntegrityViolationException {
26         handleDuplicateUserFields(userDTO);
27 
28         // Hash the plain password coming from the DTO
29         String passwordHash = getPasswordHash(userDTO.getPassword());
30         User user = new User(userDTO.getUsername(), passwordHash);
31         // All users that register through the service have to be verified
32         user.setEnabled(false);
33 
34         // Save the user so the verificationToken can be stored.
35         user = userRepository.saveAndFlush(user);
36 
37         generateAndSendToken(request, user, userDTO.getOrderId());
38 
39         return user;
40     }

41     private void handleDuplicateUserFields(UserDTO userDTO) throws DataIntegrityViolationException {
42         // Check if the username already exists
43         userRepository.findOneByUsernameIgnoreCase(userDTO.getUsername()).ifPresent(u -> {
44             throw new DataIntegrityViolationException("username already in use");
45         });
46     }
47

It's actually pretty straightforward plain java which I'll leave you to explore on your own. One thing to note is that this the only place in the application where you'll deal with a plaintext password. It's stored as a hash in the database and shouldn't be needed anywhere else in the application. The generateAndSendToken method is a bit more interesting:

48     private void generateAndSendToken(HttpServletRequest request, User user, Long orderId) {
49         // Create a new Verificationcode with this UUID, and link it to the user
50         VerificationToken verificationToken = new VerificationToken(user);
51         verificationTokenRepository.saveAndFlush(verificationToken);
52
53         // Build the URL and send this to the mailservice for sending.
54         String confirmUrl = requestUrl + "?token=" + verificationToken.getToken();
55         if (orderId != null) {
56             confirmUrl += "&orderId=" + orderId;
57         }
58         mailService.sendVerificationmail(user, confirmUrl);
59     }

Anything that requires user verification input is done using our Token class. This is part of our security, which we'll discuss later. However, this is also the part where we're using our HttpServletRequest we passed in our Controller. As you can see, we use this to extract the URL the user originally came from in order to create a clickable URL in the mail that actually takes you to the right website (without hardcoding anything!).

So, by now we created a new User with the username and password from the UserDTO, stored it in the database (using userRepository.saveAndFlush(user); and sent an email to the user for verification.

All that's left now is sending a response to the request we received! For that, we go back to the UserController:

14    @RequestMapping(method = RequestMethod.POST)
15    public ResponseEntity<?> add(HttpServletRequest request, @Validated @RequestBody UserDTO input) {
16        User save = userService.create(input, request);
17
18        // Create headers to set the location of the created User object.
19        HttpHeaders httpHeaders = new HttpHeaders();
20        httpHeaders.setLocation(
21                ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(save.getId()).toUri());
22
23        return createResponseEntity(HttpStatus.CREATED, httpHeaders,
24                "User successfully created at " + httpHeaders.getLocation(), save);
25    }

As we're following REST design principles, we want to send a HttpStatus 201 CREATED back, with the location of the newly created user in the Location header. Line 21 and 22 look complicated, but all it does is adding the User's ID field to the current request. (This will result in something like "https://api.areafiftylan.nl/users/2342". We have a helper class to make a standard ResponseEntity with a pre-defined format so the front-end can handle it properly. As you can see, we're returning a response with the 201 CREATED status code, the headers containing a Location: with a link to our new User, a message stating what we've done, and the actual User object. This will result in a neatly formatted JSON file with everything we want the front-end to know!

Of course, in this case the User is not done, as we require email validation. This will be done with a new request, which goes through our application in the same way! This is a bit more complicated than what we've seen here, but try to figure it out yourself! The relevant mapping can be found here

Clone this wiki locally