0% found this document useful (0 votes)
243 views131 pages

Practical Guide To Building An API Backend With Spring Boot - v200 1709052422392

Uploaded by

jrojas
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
243 views131 pages

Practical Guide To Building An API Backend With Spring Boot - v200 1709052422392

Uploaded by

jrojas
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 131

Practical Guide to Building

an API Back End with Spring


Boot

Wim Deblauwe
Version 2.0.0 | 2024-02-24
Practical Guide to Building an API Back End with Spring Boot

© 2024 Wim Deblauwe. All rights reserved. Version 2.0.0.

Published by C4Media, publisher of InfoQ.com.

No part of this publication may be reproduced, stored in a retrieval system or transmitted in any form
or by any means, electronic, mechanical, photocopying, recoding, scanning or otherwise except as
permitted under Sections 107 or 108 of the 1976 United States Copyright Act, without the prior written
permission of the publisher.

Production Editor: Ana Ciobotaru


Copy Editor: Lawrence Nyveen
Cover and Interior Design: Dragos Balasoiu

Library of Congress Cataloguing-in-Publication Data: ISBN: 978-0-359-04452-8


Table of Contents
Dedication. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Preface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
What is in an InfoQ mini-book? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Who this book is for. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
What you need for this book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Reader feedback . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
What Is Spring Boot? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Spring Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Spring Boot. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Getting Started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Preparation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
Spring Initializr. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
Properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Spring profiles. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Configure logging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Source code for the book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
CopsBoot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Explanation of the sample project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Generating the project skeleton . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
User Management. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
User domain . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
User repository . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
User-domain refactoring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
REST API Security . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Introduction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
Authorization server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
Testing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Application Users . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Introduction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
Link Keycloak user to application user. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
User roles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
Writing API documentation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Refactoring to avoid duplication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
Working with a Real Database. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
Installation of PostgreSQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
Using PostgreSQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
Updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
Integration testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
Validation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
Built-in validators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
Unit test for a built-in validator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
Handling validation errors via an exception handler. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
Custom field validator. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Custom object validator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
Custom object validator using a Spring service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
File Upload . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
Upload a file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
File-size validation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
Action! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
Additional reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
About the Author . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
Dedication

Dedication
//////////////////////////////////////////////////////

I would like to dedicate this book to my wife Sofie and sons Victor and Jules, as I was more absent
while trying to put my knowledge into this book. Their continued support for all my endeavours
means the world to me.

1
Practical Guide to Building an API Back End with Spring Boot

Acknowledgements
I would also like to thank all the people that made Spring and Spring Boot a reality. It is really an
amazing piece of software.

I also want to send a big thank you to the people that created Asciidoctor. It made writing this book
extremely enjoyable.

Finally, I also want to thank the book’s editors, Ben Evans and Lawrence Nyveen. Their feedback has
been invaluable for making this book the best it can be.

2
Preface

Preface
This book is the culmination of multiple years of working with Spring Boot on a variety of projects.
This is the book I wished I had when starting out building back-end applications with Java and Spring
Boot.

Through the creation of a fictional application called CopsBoot, you will learn about Spring, Spring
Boot, Spring Security, and Spring Data. You will also learn to use unit and integration tests to ensure
the proper code functionality and build a maintainable code base that you can expand upon.

This book assumes you have enough basic Java knowledge to be able to follow. However, proficiency in
similar languages like C# should be sufficient.

The first version of this book was released in 2018. A lot has changed in the past five years, so it was
time to update the book for Spring Boot 3. Security-related things have changed a lot, so those sections
are all re-written from the ground up. The biggest change there was certainly going from using the
OAuth2 password flow (which is now deprecated) to the Authorization Code flow with a separate
Authorization Server.

What is in an InfoQ mini-book?


InfoQ mini-books are designed to be concise, intending to serve technical architects looking to get a
firm conceptual understanding of a new technology or technique in a quick yet in-depth fashion. You
can think of these books as covering a topic strategically or essentially. After reading a mini-book, the
reader should have a fundamental understanding of a technology, including when and where to apply
it, how it relates to other technologies, and an overall feeling that they have assimilated the combined
knowledge of other professionals who have already figured out what this technology is about. The
reader will then be able to make intelligent decisions about the technology once their projects require
it, and can delve into sources of more detailed information (such as larger books or tutorials) at that
time.

Who this book is for


This book is for Java developers that want to quickly start creating a REST API using Spring Boot.

What you need for this book


To try code samples in this book, you will need a computer running an up-to-date operating system
(Windows, Linux, or macOS). You will need Java installed. The book code was tested against JDK 17, but
newer versions should also work.

Conventions

3
Practical Guide to Building an API Back End with Spring Boot

We use a number of typographical conventions within this book that distinguish between different
kinds of information.

Code in the text, including commands, variables, file names, CSS class names, and property names are
shown as follows:

Spring Boot uses a public static void main entry point that launches an embedded web server for
you.

A block of code is set out as follows. It may be coloured, depending on the format in which you’re
reading this book.

Listing 1. src/main/java/demo/DemoApplication.java

@RestController
class BlogController {
private final BlogRepository repository;

// Yay! No annotations needed for constructor injection in Spring 4.3+.


public BlogController(BlogRepository repository) {
this.repository = repository;
}

@RequestMapping("/blogs")
Collection<Blog> list() {
return repository.findAll();
}
}

When we want to draw your attention to certain lines of code, those lines are annotated using
numbers accompanied by brief descriptions.

@SpringBootApplication ①
public class Application {

public static void main(String[] args) {


SpringApplication.run(Application.class, args);
}
}

① The @SpringBootApplication annotation marks this class as the main class when starting with Spring
Boot.

4
Preface

 Tips are shown using callouts like this.

 Warnings are shown using callouts like this.

Sidebar

Additional information about a certain topic may be displayed in a sidebar like this one.

Finally, this text shows what a quote looks like:

In the end, it’s not the years in your life that count. It’s the life in your years.

— Abraham Lincoln

Reader feedback
We always welcome feedback from our readers. Let us know what you thought about this book —
what you liked or disliked. Reader feedback helps us develop titles that deliver the most value to you.

To send us feedback, e-mail us at [email protected].

If you’re interested in writing a mini-book for InfoQ, see http://www.infoq.com/minibook-guidelines.

5
Practical Guide to Building an API Back End with Spring Boot

Introduction
I have been working with Spring Boot for many years, and it has made development a tremendous joy.
It is important in our fast-paced world to be able to prototype quickly, but also to ensure that you are
not doing any wasted work.

For me, this is one of the major strengths of Spring Boot. The smallest application can fit in a tweet, yet
your application will scale to Internet scale with the greatest of ease.

Combine Spring Boot with Spring Data and Spring Security, and you can have something up and
running in no time. And it is not just "something", it is a solid base to build upon.

6
What Is Spring Boot?

PART
ONE
What Is Spring Boot?

7
Practical Guide to Building an API Back End with Spring Boot

Spring Framework
Spring Boot is based upon the Spring Framework, which is at its core a dependency-injection
container. Spring makes it easy to define everything in your application as loosely coupled components
which Spring will tie together at run time. Spring also has a programming model that allows you to
make abstractions from specific deployment environments.

One of the key things you need to understand is that Spring is based on the concept of "beans" or
"components", which are basically singletons without the drawbacks of the traditional singleton
pattern.

With dependency injection, each component just declares the collaborators it needs, and Spring
provides them at run time. The biggest advantage is that you can easily inject different instances for
different deployment scenarios of your application (e.g., staging versus production versus unit tests).

Spring Boot
The Spring Boot website explains itself succinctly:

Spring Boot makes it easy to create stand-alone, production-grade Spring based


Applications that you can "just run". We take an opinionated view of the Spring
platform and third-party libraries so you can get started with minimum fuss.
Most Spring Boot applications need very little Spring configuration.

With Spring Boot, you get up and running with your Spring application in no time, without the need to
deploy to a container like Tomcat or Jetty. You can just run the application right from your IDE.

Spring Boot also ensures that you get a list of versions of libraries inside and outside of the Spring
portfolio that are guaranteed to work together without problems.

8
Getting Started

PART
TWO
Getting Started

9
Practical Guide to Building an API Back End with Spring Boot

Preparation
Before you can start, you need to install some things:

1. Java: Download the JDK 17 from https://www.oracle.com/java/technologies/downloads/

2. Maven: Follow the instructions at https://maven.apache.org/install.html to install Maven.

If you are on a UNIX-based platform, you might want to use SDKMAN! for the
 installation of Java and Maven.

Spring Initializr
The easiest way to get started with Spring Boot is to create a project using Spring Initializr. This web
application allows you to generate a Spring Boot project with the option of including all the
dependencies you need.

To get started, open your favorite browser at https://start.spring.io/

Figure 1. The Spring Initializr website.

10
Getting Started

Select the following options:

• Maven as build system;

• Java 17 as programming language; and

• Spring Boot version 3.2.2, as this is the most recent version at the time of writing.

You can use Gradle as your build system or one of the other supported JVM languages
 if you prefer to. The main principles explained in the book remain the same.

Start out simple and only include Web as a dependency.

Press "Generate Project" and unzip the file to a location of your choice.

The generated project contains the following files and directories:

pom.xml ①
mvnw ②
mvnw.cmd
HELP.md ③
.gitignore ④
src
|-- main
|-- java
|-- com.springbook.application
|-- Application ⑤
|-- resources
|-- application.properties
|-- test
|-- java
|-- com.springbook.application
|-- ApplicationTests ⑥

① pom.xml defines the Maven configuration of the project.

② mvnw and mvnw.cmd allow the project to build even if you don’t have Maven installed via the Maven
Wrapper.

③ The HELP.md file has some links to documentation on the dependencies you have selected.

④ The generated .gitignore has some good defaults depending on the options you have selected (e.g.,
ignoring the target folder if you selected Maven).

⑤ The starting point of the application has a main() that lets you run and debug the application from
your favorite IDE without having to deploy to a container like Tomcat.

⑥ A unit test starts your application to check if at least the Spring context loads properly.

Let’s take a closer look at Application.java:

11
Practical Guide to Building an API Back End with Spring Boot

Listing 2. Application.java

@SpringBootApplication ①
public class Application {

public static void main(String[] args) {


SpringApplication.run(Application.class, args);
}
}

① The @SpringBootApplication annotation marks this class as the main class when starting with Spring
Boot.

Not much code, right? However, if you run this main, you will notice that a lot of things are happening
under the hood.

You can run the application from your IDE or through Maven with mvn spring-boot:run. See Running
your application in the Spring Boot documentation for more information.

[INFO] --- spring-boot:3.2.2:run (default-cli) @ application ---


[INFO] Attaching agents: []

. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.2)

2024-02-24T10:08:48.181+01:00 INFO 12281 --- [ main]


com.springbook.application.Application : Starting Application using Java 21.0.2 with
PID 12281 (/Users/wdb/Projects/personal/spring-boot-book/src/example-code/chapter02/01 -
Generated project/target/classes started by wdb in /Users/wdb/Projects/personal/spring-
boot-book/src/example-code/chapter02/01 - Generated project)
2024-02-24T10:08:48.184+01:00 INFO 12281 --- [ main]
com.springbook.application.Application : No active profile set, falling back to 1
default profile: "default"
2024-02-24T10:08:48.951+01:00 INFO 12281 --- [ main]
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2024-02-24T10:08:48.964+01:00 INFO 12281 --- [ main]
o.apache.catalina.core.StandardService : Starting service [Tomcat]
2024-02-24T10:08:48.965+01:00 INFO 12281 --- [ main]
o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache
Tomcat/10.1.18]
2024-02-24T10:08:49.016+01:00 INFO 12281 --- [ main]

12
Getting Started

o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded


WebApplicationContext
2024-02-24T10:08:49.016+01:00 INFO 12281 --- [ main]
w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization
completed in 785 ms
2024-02-24T10:08:49.353+01:00 INFO 12281 --- [ main]
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with
context path ''
2024-02-24T10:08:49.361+01:00 INFO 12281 --- [ main]
com.springbook.application.Application : Started Application in 1.535 seconds (process
running for 1.913)

You have a fully working application that starts an embedded Tomcat on port 8080. It also configures
logging to the console.

Spring Boot analyses what is on the class path and will enable functionality based on this. In this case,
you have Tomcat on the class path due to the spring-boot-starter-web dependency in the pom.xml:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

Properties
Spring Boot lets you configure various parts of your application via the application.properties file. To
try this out, add the following line in the file:

server.port=8888

After restarting the application, the logging output will show that your application now runs on that
different port:

Tomcat initialized with port(s): 8888 (http)

You can view a list of the most common properties at Appendix A. Common
 application properties of the Spring Boot reference documentation.

There are two other important ways to configure properties: with the command line or with
environment variables

To change the port using the command line, do the following:

13
Practical Guide to Building an API Back End with Spring Boot

java -jar application.jar --server.port=8888

To change the port with an environment variable, you can define SERVER_PORT and your application will
pick this up. Notice how you can use capitals and an underscore in the environment variable and
Spring Boot will still pick it up. This feature is called relaxed binding.

See Externalized Configuration for all the ways you can configure properties with Spring Boot.

Spring profiles
Spring profiles let you selectively enable or disable parts of your application. The Spring Boot
documentation says that Spring profiles "provide a way to segregate parts of your application
configuration and make it only available in certain environments".

For example, you can have an EmailGateway interface with a LoggingEmailGateway, SendGridEmailGateway
and SmtpEmailGateway implementations. You could then use the LoggingEmailGateway during
development, SmtpEmailGateway in staging, and SendGridEmailGateway for production.

The staging and production environment should be as closely aligned as possible. So


 this example is more for the purposes of illustration rather than a recommendation of
best practice.

Only enabling a class if a profile is active

This code shows how to define a singleton in Spring and have Spring create an instance only if the dev
profile is active:

@Component
@Profile("dev")
public class LoggingEmailGateway implements EmailGateway {
...
}

To test this, you must set the active profile when running your application. You can do this by setting
spring.profiles.active in application.properties or you can pass it as a program argument. For
example:

java -jar application.jar --spring.profiles.active=dev

Some IDEs have built-in support for activating profiles. This is a screenshot of the run

 configuration dialogue of Jetbrains' IntelliJ IDEA:

14
Getting Started

Spring profile-specific properties

By default, Spring Boot will apply the properties defined in application.properties on the root of the
class path. If you want to set specific settings only for when a certain profile is active, you can use the
naming convention application-<profileName>.properties.

For example, to set the web listening port to 5000 for production:

Listing 3. application-prod.properties

server.port=5000

Another common use case for this is specifying the database connection URL since this changes for the
various environments your application will run in.

Configure logging
Logging for the production code

Spring Boot uses Logback for logging by default, and logs everything to the console. While this is great
when running in your IDE, you might want to log things to a file for staging or production.

To configure this, create a logback-spring.xml file:

<?xml version="1.0" encoding="UTF-8"?>


<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/> ①
<springProfile name="dev,local">②
<include resource="org/springframework/boot/logging/logback/console-appender.xml"
/>
<root level="INFO">
<appender-ref ref="CONSOLE" />

15
Practical Guide to Building an API Back End with Spring Boot

</root>
</springProfile>

<springProfile name="staging,prod">③
<include resource="org/springframework/boot/logging/logback/file-appender.xml"/>
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
</springProfile>
</configuration>

① These are the Spring Boot defaults.

② If the Spring profile dev or local is enabled, then log to console.

③ If the Spring profile staging or prod is enabled, then log to file.

This is the minimal configuration needed to easily switch between console and file and still allow
Spring Boot properties to influence what is logged.

To set the name of the log file, adjust application-staging.properties or application-prod.properties:

logging.file=my-application.log
logging.level.root=INFO

Logging for the test code

To configure the logging for the test code, we can rely on the default behaviour of Logback to search
for a logback-test.xml file on the class path.

By putting this file in src/test/resources, it will be first on the class path when the tests are run and
used automatically by Logback.

This is a sample file that logs to the console and sets the default level for the loggers of your own
application to DEBUG:

<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{YYYY-MM-dd HH:mm:ss} %level [%thread] %logger{0} -
%msg%n%ex</pattern>
</encoder>
</appender>

<root level="WARN">

16
Getting Started

<appender-ref ref="STDOUT"/>
</root>
<logger name="com.springbook.application"> ①
<level value="DEBUG"/>
</logger>
<logger name="org.hibernate">
<level value="WARN"/>
</logger>
<logger name="org.hibernate.type">
<level value="WARN"/> <!-- set to TRACE to view parameter binding in queries -->
</logger>
<logger name="org.springframework.security">
<level value="WARN"/>
</logger>

</configuration>

① This configures loggers of com.springbook.application and its sub-packages to DEBUG.

If you run mvn clean verify now, you will get lots of output. If you were to add some logging of your
own, it would also show in the Maven output:

[INFO] --- surefire:3.1.2:test (default-test) @ application ---


[INFO] Using auto detected provider
org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.springbook.application.ApplicationTests
10:06:22.309 [main] INFO
org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not
detect default configuration classes for test class
[com.springbook.application.ApplicationTests]: ApplicationTests does not declare any
static, non-private, non-final, nested classes annotated with @Configuration.
10:06:22.403 [main] INFO
org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found
@SpringBootConfiguration com.springbook.application.Application for test class
com.springbook.application.ApplicationTests

. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/

17
Practical Guide to Building an API Back End with Spring Boot

:: Spring Boot :: (v3.2.2)

2024-02-24T10:06:22.754+01:00 INFO 12220 --- [ main]


c.s.application.ApplicationTests : Starting ApplicationTests using Java 21.0.2
with PID 12220 (started by wdb in /Users/wdb/Projects/personal/spring-boot-
book/src/example-code/chapter02/01 - Generated project)
2024-02-24T10:06:22.755+01:00 INFO 12220 --- [ main]
c.s.application.ApplicationTests : No active profile set, falling back to 1
default profile: "default"
2024-02-24T10:06:23.652+01:00 INFO 12220 --- [ main]
c.s.application.ApplicationTests : Started ApplicationTests in 1.117 seconds
(process running for 2.085)
WARNING: A Java agent has been loaded dynamically
(/Users/wdb/.m2/repository/net/bytebuddy/byte-buddy-agent/1.14.11/byte-buddy-agent-
1.14.11.jar)
WARNING: If a serviceability tool is in use, please run with
-XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with
-Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.370 s -- in
com.springbook.application.ApplicationTests
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

You probably don’t want the output of your tests to appear when running with Maven. To have Maven
put all the output in a file, use the following configuration for the Maven Surefire Plugin:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<redirectTestOutputToFile>true</redirectTestOutputToFile> ①
<printSummary>false</printSummary>
</configuration>
</plugin>

① Set the redirectTestOutputToFile to true so Surefire will put the output of each test in a separate file.

Source code for the book


If you ever get stuck following along, you can refer to the full source code on GitHub:

18
Getting Started

https://github.com/wimdeblauwe/spring-boot-building-api-backend

19
Practical Guide to Building an API Back End with Spring Boot

PART
THREE
CopsBoot

20
CopsBoot

Explanation of the sample project


The sample application upon which the examples in this this book are based is entirely fictional but
will serve well to demonstrate all the important concepts.

The application, called CopsBoot, supports a police force in their daily work. It allows officers on the
road to interact with the server via a mobile application. Through the mobile app, officers can report
crimes and attach images to their reports.

This book will not deal with the development of the mobile application itself but will show how the API
will work using Postman. Spring REST Docs will generate some beautiful documentation that should
contain all the info an app developer would need to get started.

Generating the project skeleton


To get started, use Spring Initializr at https://start.spring.io with the following values:

Property Value

Build system Maven Project

Programming language Java

Spring Boot version 3.2.2

Artifact Group com.example

Artifact copsboot

Java version 17

Search for dependencies Web, Security, JPA, Validation, H2

Unzip the generated .zip file to a location of your choice.

The dependencies section of the pom.xml will look like this:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId> ①
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId> ②
</dependency>
<dependency>

21
Practical Guide to Building an API Back End with Spring Boot

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId> ③
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> ④
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId> ⑤
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId> ⑥
<scope>test</scope>
</dependency>
</dependencies>

① JPA is the Java Persistence API. This starter pulls in Hibernate, which you will use to persist your
entities to the database.

② This adds support for Spring Security so you can have authorization and authentication.

③ This allows the use of bean validation annotations to validate entities before persisting them, or to
validate request objects in rest controllers before handling them.

④ This adds support for web and REST controllers for building your API.

⑤ H2 is an in-memory database that will be used for running the application locally and for unit
testing.

⑥ These are helper classes for testing your Spring Security configuration.

22
User Management

PART
FOUR
User Management

23
Practical Guide to Building an API Back End with Spring Boot

The first thing to add to your application is user management and security.

User domain
Create a class User to hold all properties of your users. For now, start out simple and use only the
following properties:

Property Description

Name The full name of the user.

Email The email address of the user, which will also


serve as the username for login.

Password The user’s password.

Role The user’s role in the system, which defines what


a user can and can’t do.

Start with the following code for User:

Listing 4. User.java

package com.example.copsboot.user;

import java.util.Set;
import java.util.UUID;

public class User {


private UUID id;
private String email;
private String password;
private Set<UserRole> roles;

public User(UUID id, String email, String password, Set<UserRole> roles) {


this.id = id;
this.email = email;
this.password = password;
this.roles = roles;
}

public UUID getId() {


return id;
}

public String getEmail() {


return email;

24
User Management

public String getPassword() {


return password;
}

public Set<UserRole> getRoles() {


return roles;
}
}

UserRole defines the roles of users in the application:

Listing 5. UserRole.java

package com.example.copsboot.user;

public enum UserRole {


OFFICER,
CAPTAIN,
ADMIN
}

To keep the example simple, create only three roles:

• Officer is a police officer who does the field work.

• Captain is the boss of the officers.

• Admin is an administrative role that has access to all parts of the application.

Notice how we use the com.example.copsboot.user package for our domain class. Some
developers prefer packages by layer, which groups types of similar classes like
domain classes in a domain package, services in a service package, etc.

Using packages by feature, however, makes the core abstractions of the project
immediately visible on the package tree.

To persist your first domain class using Spring Boot Data JPA, you must add some annotations from the
Java Persistence API (JPA) specification:

package com.example.copsboot.user;

import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;

25
Practical Guide to Building an API Back End with Spring Boot

import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import java.util.Set;
import java.util.UUID;

@Entity ①
@Table(name = "copsboot_user") ②
public class User {
@Id
private UUID id; ③

private String email;


private String password;

@ElementCollection(fetch = FetchType.EAGER)
@Enumerated(EnumType.STRING)
@NotNull
private Set<UserRole> roles; ④

protected User() { ⑤

public User(UUID id, String email, String password, Set<UserRole> roles) {


this.id = id;
this.email = email;
this.password = password;
this.roles = roles;
}

① @Entity marks the class as a persistable entity for JPA.

② The @Table annotation is optional. It allows you to explicitly set the name to be used for the database
table. If you do not specify a name, Spring Boot will convert the name of the class to snake_case.

③ The id field is annotated with @Id to mark it as a primary key of the entity.

④ The roles field is a collection of enum values. @Enumerated(EnumType.STRING) ensures the enum
values are stored as string values.

⑤ Hibernate needs a no-argument constructor, so this adds one. It does not need to be public, so this
keeps it protected.

This application uses "early primary key" generation. This means that it does not rely on the database
to provide a primary key, but first creates a primary key, which passes into the constructor of your User

26
User Management

object. The main advantages of this are that you never have "incomplete" objects, and it makes it easier
to implement equals.

User repository
The actual persistence happens through a repository. The heavy lifting for the repository pattern has
been implemented in Spring Data. Using it is as simple as defining an interface that extends from
CrudRepository:

package com.example.copsboot.user;

import org.springframework.data.repository.CrudRepository;

import java.util.UUID;

public interface UserRepository extends CrudRepository<User, UUID> {


}

By creating this interface, you have a repository at run time that allows you to save, edit, delete, and
find User entities.

To check that everything works, create a test for it:

package com.example.copsboot.user;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.HashSet;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest ①
public class UserRepositoryTest {

@Autowired
private UserRepository repository; ②

@Test
public void testStoreUser() { ③
HashSet<UserRole> roles = new HashSet<>();

27
Practical Guide to Building an API Back End with Spring Boot

roles.add(UserRole.OFFICER);
User user = repository.save(new User(UUID.randomUUID(), ④
"[email protected]",
"my-secret-pwd",
roles));
assertThat(user).isNotNull(); ⑤

assertThat(repository.count()).isEqualTo(1L); ⑥
}
}

① @DataJpaTest instructs the testing support to start only the part of the application responsible for
everything related to JPA.

② Inject the UserRepository so you can use it in the unit test.

③ This is the method that contains your test.

④ Save the User entity in the database here.

⑤ The object returned from the save method of the repository should return a non-null object.

⑥ If you count the number of User entities in the database, you should have one.

The act of starting only part of the application context in a unit test is what Spring
 calls test slicing. Doing it makes the unit tests faster and more focused as fewer
components need to be bootstrapped.

The unit test uses AssertJ as an assertion framework. Its tagline is "fluent assertions for Java" and that
is exactly what it is and why I like it so much. It is included by default via the spring-boot-starter-test
dependency.

If you want to learn more about what is included in the spring-boot-starter-test,


 then have a look to Guide to Testing With the Spring Boot Starter Test.

You need a database before you can run the test. For tests like these, the easiest way to get one is by
putting the H2 database on the classpath. If we do that, Spring Boot will create an instance of it and use
it in the test. This dependency should normally already be present in the pom.xml:

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope> ①
</dependency>

① The dependency has been added with runtime scope (as opposed to test scope) as you will also start
the application itself with H2 if you run it with the dev profile.

28
User Management

Running the test should succeed:

Instead of using H2, you can use your real database such as PostgreSQL with Docker
 and Testcontainers. We will explore how to do that in the chapter Working with a Real
Database.

User-domain refactoring
You can now save your User entities in the database. However, implementing the following changes
can improve the maintainability of the code:

1. Use a dedicated class for the primary key.

2. Extract a superclass for all entities so that the primary key is defined in a consistent way.

3. Centralize the primary-key generation in the repository.

Dedicated primary-key class

Most examples use either a long or a UUID as the primary key for an entity. One alternative, as
described in Implementing Domain Drive Design by Vaughn Vernon, is to use a dedicated class for
primary keys. This has the following advantages:

• It more clearly expresses the intent. If a variable is of type UserId, it is clear what you are are
talking about, as opposed to a simple long or UUID.

• It is impossible to assign a value that is a UserId to an OrderId or a BookId. This reduces the chance
of putting a wrong ID somewhere.

• If you want to change from UUID to long or vice versa for the primary key, you will be able to do so
with minimal changes to the application code.

Create this AbstractEntityId class as the basis for all the ID classes in your application:

Listing 6. AbstractEntityId.java

package com.example.orm.jpa;

29
Practical Guide to Building an API Back End with Spring Boot

import com.example.util.ArtifactForFramework;

import jakarta.persistence.MappedSuperclass;
import java.io.Serializable;
import java.util.Objects;

import static com.google.common.base.MoreObjects.toStringHelper;

@MappedSuperclass
public abstract class AbstractEntityId<T extends Serializable> implements Serializable,
EntityId<T> {
private T id;

@ArtifactForFramework
protected AbstractEntityId() {
}

protected AbstractEntityId(T id) {


this.id = Objects.requireNonNull(id);
}

@Override
public T getId() {
return id;
}

@Override
public String asString() {
return id.toString();
}

@Override
public boolean equals(Object o) {
boolean result = false;

if (this == o) {
result = true;
} else if (o instanceof AbstractEntityId) {
AbstractEntityId other = (AbstractEntityId) o;
result = Objects.equals(id, other.id);
}

return result;
}

@Override

30
User Management

public int hashCode() {


return Objects.hash(id);
}

@Override
public String toString() {
return toStringHelper(this)
.add("id", id)
.toString();
}
}

The empty constructor is annotated with @ArtifactForFramework. Create this


annotation to indicate that the constructor is solely there for a framework that needs
it but is not intended to be used by the application itself.

The code of the annotation itself is simple:

import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;

@Retention(value = RetentionPolicy.SOURCE)
public @interface ArtifactForFramework {
}

An extra benefit is that in IntelliJ IDEA, for example, you can indicate that elements
annotated with this annotation should not be marked as unused.

To make AbstractEntityId compile, you need to add Guava as a dependency:

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>

Next, define AbstractEntity. This will be the base class for your entities and ensures that they will use
your EntityId:

Listing 7. AbstractEntity.java

package com.example.orm.jpa;

import com.example.util.ArtifactForFramework;

31
Practical Guide to Building an API Back End with Spring Boot

import jakarta.persistence.EmbeddedId;
import jakarta.persistence.MappedSuperclass;
import java.util.Objects;

import static com.google.common.base.MoreObjects.toStringHelper;


import static com.google.common.base.Preconditions.checkNotNull;

/**
* Abstract super class for entities. We are assuming that early primary key
* generation will be used.
*
* @param <T> the type of {@link EntityId} that will be used for this entity
*/
@MappedSuperclass
public abstract class AbstractEntity<T extends EntityId> implements Entity<T> {

@EmbeddedId
private T id;

@ArtifactForFramework
protected AbstractEntity() {
}

public AbstractEntity(T id) {


this.id = checkNotNull(id);
}

@Override
public T getId() {
return id;
}

@Override
public boolean equals(Object obj) {
boolean result = false;

if (this == obj) {
result = true;
} else if (obj instanceof AbstractEntity) {
AbstractEntity other = (AbstractEntity) obj;
result = Objects.equals(id, other.id);
}

return result;

32
User Management

@Override
public int hashCode() {
return Objects.hash(id);
}

@Override
public String toString() {
return toStringHelper(this)
.add("id", id)
.toString();
}
}

For completeness, here is the interface implementation of EntityId and Entity:

Listing 8. EntityId.java

package com.example.orm.jpa;

import java.io.Serializable;

/**
* Interface for primary keys of entities.
*
* @param <T> the underlying type of the entity id
*/
public interface EntityId<T> extends Serializable {

T getId();

String asString(); ①
}

① The asString method returns the ID as a string representation, for use in a URL for example. You are
not using toString because that is usually for debugging purposes while you will need to use this as
part of your application logic.

Listing 9. Entity.java

package com.example.orm.jpa;

/**
* Interface for entity objects.

33
Practical Guide to Building an API Back End with Spring Boot

*
* @param <T> the type of {@link EntityId} that will be used in this entity
*/
public interface Entity<T extends EntityId> {

T getId();
}

With all this in place, you can now refactor your User class. First, create a UserId:

Listing 10. UserId.java

package com.example.copsboot.user;

import com.example.orm.jpa.AbstractEntityId;

import java.util.UUID;

public class UserId extends AbstractEntityId<UUID> {

protected UserId() { ①

public UserId(UUID id) { ②


super(id);
}
}

① Hibernate needs the protected no-args constructor to work.

② This is the constructor that the application code should use.

In the User class itself, you can remove the id field and its getter since it is now part of the
AbstractEntity superclass. This is how it looks after the refactoring:

@Entity
@Table(name = "copsboot_user")
public class User extends AbstractEntity<UserId> {

With the following constructor:

public User(UserId id, String email, String password, Set<UserRole> roles) {


super(id);

34
User Management

this.email = email;
this.password = password;
this.roles = roles;
}

Centralize primary-key generation

In your unit test, you manually created the primary key by calling UUID.randomUUID(). This could be fine
for a UUID, but certainly not if you want to use long, for example. For this reason, add a method on the
UserRepository that will give you the "next" ID to use if you want to create an entity.

Since UserRepository is an interface, you need to do some additional work to make this possible. To get
started, you need to create a UserRepositoryCustom interface:

package com.example.copsboot.user;

public interface UserRepositoryCustom {


UserId nextId();
}

The nextId method will return a new UserId instance each time it is called. Step 2 adds this interface to
the UserRepository interface:

public interface UserRepository extends CrudRepository<User, UserId>,


UserRepositoryCustom {
}

Step 3 creates a UserRepositoryImpl class that implements the UserRepositoryCustom interface method:

package com.example.copsboot.user;

import com.example.orm.jpa.UniqueIdGenerator;

import java.util.UUID;

public class UserRepositoryImpl implements UserRepositoryCustom {


private final UniqueIdGenerator<UUID> generator;

public UserRepositoryImpl(UniqueIdGenerator<UUID> generator) {


this.generator = generator;
}

@Override

35
Practical Guide to Building an API Back End with Spring Boot

public UserId nextId() {


return new UserId(generator.getNextUniqueId());
}
}

When the application runs, Spring Data will combine your own UserRepositoryImpl code with Spring
Data’s CrudRepository code so the methods from both UserRepositoryCustom and CrudRepository are
available everywhere you inject a UserRepository.

The generation of the unique UUID is put behind the UniqueIdGenerator interface. This might seem like
a bit overkill for the UUID case, but you certainly need it if you want to use long values. You might want
to get them from the database or maybe from some distributed component that can hand out unique
IDs across connected JVMs. In that case, having this generation centralized in the UniqueIdGenerator is a
big plus.

If you want to use this for your own projects, you can depend on the JPearl library
 which I have open sourced.

Updating the unit test

The test method itself now needs to change to use the nextId method from the repository:

@Test
public void testStoreUser() {
HashSet<UserRole> roles = new HashSet<>();
roles.add(UserRole.OFFICER);
User user = repository.save(new User(repository.nextId(),
"[email protected]",
"my-secret-pwd",
roles));
assertThat(user).isNotNull();

assertThat(repository.count()).isEqualTo(1L);
}

However, this is not enough, as you quickly see when you try to run the test:

java.lang.IllegalStateException: Failed to load ApplicationContext

Digging a bit more in the stacktrace will reveal this:

org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of


type 'com.example.orm.jpa.UniqueIdGenerator<java.util.UUID>' available: expected at least

36
User Management

1 bean which qualifies as autowire candidate.

The cause of the error is straightforward. Your UserRepositoryImpl needs a UniqueIdGenerator for
constructor injection, but there does not seem to be an instance in the application context. Spring Boot
will not create an instance of UniqueIdGenerator because you did not ask it to.

You could annotate InMemoryUniqueIdGenerator with @Component, but there is a different way especially
for unit tests. You can create a static inner class in your unit-test class and annotate it with
@TestConfiguration. You can then use @Bean annotated methods to define singletons that should be
available in the unit test. For your test, that looks like this:

Listing 11. UserRepositoryTest.java

@TestConfiguration
static class TestConfig {
@Bean
public UniqueIdGenerator<UUID> generator() {
return new InMemoryUniqueIdGenerator();
}
}

After this, your unit test will be green.

As a final step, you also need to make such a bean available in your application. To do this, add the
same bean declaration to a new class CopsbootApplicationConfiguration.java:

package com.example.copsboot;

import com.example.orm.jpa.InMemoryUniqueIdGenerator;
import com.example.orm.jpa.UniqueIdGenerator;
import java.util.UUID;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CopsbootApplicationConfiguration {

@Bean
public UniqueIdGenerator<UUID> uniqueIdGenerator() {
return new InMemoryUniqueIdGenerator();
}
}

37
Practical Guide to Building an API Back End with Spring Boot

Summary
You have seen how to store an entity using Spring Data JPA and how to test it.

38
REST API Security

PART
FIVE
REST API Security

39
Practical Guide to Building an API Back End with Spring Boot

Introduction
Security is a broad topic and surely whole books have been written on that subject alone. For this
book, we will focus on getting basic security up for a mobile application.

The de-facto standard for security with mobile applications (and also many other types of applications)
is the OAuth2 standard. If you are not familiar with OAuth2, you can read a good introduction to it at
The Simplest Guide To OAuth 2.0.

OAuth2 defines a few different possible flows. We will implement the "Authorization Code" flow for
our application.

An earlier version of this book used the "Password" flow, but this is no longer
 considered a good option. See Don’t use the OAuth password grant type for more
information.

Authorization server
With OAuth2, there is a separate application that is responsible for authorization. You could write this
yourself, perhaps based on Spring Authorization Server. Alternatively, you can look at a SaaS solution
like Okta, Supabase Auth, Auth0, etc.

A third alternative is to use an open-source product like Keycloak. We can easily run this locally in a
Docker container without any cost.

Keycloak setup

To start with Keycloak, create a docker-compose.yaml file in the root of your project:

Listing 12. docker-compose.yaml

version: '3'
services:
identity:
image: 'quay.io/keycloak/keycloak:22.0.1'
entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm
ports:
- '8180:8080'
environment:
KEYCLOAK_LOGLEVEL: 'INFO'
KEYCLOAK_ADMIN: 'admin'
KEYCLOAK_ADMIN_PASSWORD: 'admin-secret'
KC_HOSTNAME: 'localhost'
KC_HEALTH_ENABLED: 'true'
KC_METRICS_ENABLED: 'true'

40
REST API Security

Start the Docker container via docker-compose up -d.

If you are not familiar with Docker, see Getting started with Docker or Docker tutorial
 for beginners.

If all goes well, you can use your browser to navigate to http://localhost:8180 and see the Keycloak
welcome page.

Click on 'Administration Console' to access the login page of your local Keycloak installation.

Use admin/admin-secret as username and password to log on.

Execute the following steps to further configure Keycloak:

1. Open the dropdown on the left that shows "master" and press the "Create Realm" button.

2. Set the "Realm name" to copsboot. A Keycloak realm contains all the users and the applications of
those users for a single architecture.

3. Select "Clients" and press "Create Client"

a. Client type: OpenID Connect

b. Client ID: copsboot-mobile-client

c. Name: Copsboot Mobile Client

d. Client Authentication: On

e. Authorization: Off

f. Only select 'Standard flow' as authentication flow. (This is the Authorization Code flow in
OAuth2 terms.)

g. Valid redirect URIs: https://oauth.pstmn.io/v1/callback. (We will be testing with Postman.)

h. Press 'Save'.

We now defined a client application which has a client id (copsboot-mobile-client) and a client secret.
The secret is automatically generated by Keycloak and can be viewed in the "Credentials" tab of the
client.

For clarity, we have 3 parties in the whole OAuth2 exchange:

• The Authorization Server: This is Keycloak in our case. It manages the users and
the roles. It can exchange credentials for tokens. It has UI to allow users to log on,
or it can delegate the logon itself to another authorization server (e.g., social login
 via Google).

• The Resource Server: This is our Spring Boot application.

• The client: This is the application that wants to read data from the Resource
Server. If we are building a mobile application, this is our mobile application.

41
Practical Guide to Building an API Back End with Spring Boot

When we test with Postman, it is Postman being the client.

Next to clients, we also need users. Let’s create a test user in Keycloak.

Select "Users" in the menu on the left side and press "Add user".

Give the user the following properties:

• Username: [email protected]

• Email: [email protected]

• First name: Wim

• Last name: Example

Press "Create" to create the user.

We now need to give our user a password. In contrast to the client, there is no default generated
password for a user. This is because you could integrate Keycloak with SSO solutions like Google, Azure
AD, or others and never store the passwords of your users in Keycloak itself. To keep things simple, we
will manually set a password for our test user.

Select the "Credentials" tab and click on "Set password". Enter a password you want to use and disable
the "Temporary" checkbox. (If you leave it enabled, the user needs to change its password after the
first login.)

Configure Spring Boot application for Keycloak

Now that Keycloak is configured, we can access the following URL in our browser:
http://localhost:8180/realms/copsboot/.well-known/openid-configuration

It should return something similar to this:

{
"issuer":"http://localhost:8180/realms/copsboot",
"authorization_endpoint":"http://localhost:8180/realms/copsboot/protocol/openid-
connect/auth",
"token_endpoint":"http://localhost:8180/realms/copsboot/protocol/openid-
connect/token",
"introspection_endpoint":"http://localhost:8180/realms/copsboot/protocol/openid-
connect/token/introspect",
"userinfo_endpoint":"http://localhost:8180/realms/copsboot/protocol/openid-
connect/userinfo",
"end_session_endpoint":"http://localhost:8180/realms/copsboot/protocol/openid-
connect/logout",
"frontchannel_logout_session_supported":true,
"frontchannel_logout_supported":true,

42
REST API Security

"jwks_uri":"http://localhost:8180/realms/copsboot/protocol/openid-connect/certs",
"check_session_iframe":"http://localhost:8180/realms/copsboot/protocol/openid-
connect/login-status-iframe.html",
"grant_types_supported":[
"authorization_code",
"implicit",
"refresh_token",
"password",
"client_credentials",
"urn:ietf:params:oauth:grant-type:device_code",
"urn:openid:params:grant-type:ciba"
],
...
}

The .well-known/openid-configuration endpoint shows all kinds of information that other applications
can use to know what URL’s can be used and what flows are enabled. We will need those URLs to
configure our Spring Boot application and for testing with Postman.

We will make our Spring Boot application a proper Resource Server by adding the following
dependency to our pom.xml:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

Point to Keycloak as the JWT token issuer by adding the following property in application.properties:

spring.security.oauth2.resourceserver.jwt.issuer-uri
=http://localhost:8180/realms/copsboot

Spring Boot will use that configured URL to get the .well-known/openid-configuration endpoint and use
the information in that endpoint to fully configure itself to work with Keycloak.

We need to define in our application what endpoints should be secured. We do this by creating a
SecurityFilterChain bean like this:

package com.example.copsboot.infrastructure.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;

43
Practical Guide to Building an API Back End with Spring Boot

import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class WebSecurityConfiguration {
@Bean
SecurityFilterChain configureSecurityFilterChain(HttpSecurity http) throws Exception
{

http
.authorizeHttpRequests(authorizeRequests -> authorizeRequests
.requestMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() ①
.requestMatchers("/api/**").authenticated() ②
.anyRequest().authenticated())
.oauth2ResourceServer(it -> it.jwt(Customizer.withDefaults())); ③

return http.build();
}
}

① Allow all OPTIONS requests without authentication.

② Any endpoint that starts with /api needs an authenticated user to be able to access it.

③ Configure our application as an OAuth2 resource server that uses JWT tokens (which will be issued
by our Keycloak authentication server).

A JWT token is an encoded JSON object that contains information about the
authenticated user. It can be verified and trusted because it is digitally signed.

Learn more about JWT tokens at https://jwt.io/.

To be able to test, we need an endpoint in our Resource Server. Let’s add one that returns some
information on the authenticated user so we know everything is working fine:

package com.example.copsboot.user.web;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController

44
REST API Security

@RequestMapping("/api/users")
public class UserRestController {
@GetMapping("/me") ①
public Map<String, Object> myself(@AuthenticationPrincipal Jwt jwt) { ②
return Map.of("subject", jwt.getSubject(), ③
"claims", jwt.getClaims());
}
}

① Declare a GET request /api/users/me.

② Inject the used JWT token via the @AuthenticationPrincipal annotation into the method.

③ Return some information taken from the JWT token in a JSON format.

Testing with Postman

Let’s make sure everything works correctly by using Postman. If you don’t have it yet, download and
install it from the website, you can use it for free.

Configure the request in Postman like this:

• URL: http://localhost:8080/api/users/me

• Authorization

◦ Type: OAuth 2.0

◦ Add authorization data to: Request headers

◦ Header prefix: Bearer

◦ Token name: you can leave this empty

◦ Grant type: Authorization Code

◦ Callback url: This should be filled in automatically by Postman and set to


https://oauth.pstmn.io/v1/callback (which is also the value we configured in Keycloak)

◦ Auth URL: This is the value of authorization_endpoint in the response of http://localhost:8180/


realms/copsboot/.well-known/openid-configuration. This should normally be
http://localhost:8180/realms/copsboot/protocol/openid-connect/auth

◦ Access Token URL: This is the value of token_endpoint in the response of http://localhost:8180/
realms/copsboot/.well-known/openid-configuration. This should normally be
http://localhost:8180/realms/copsboot/protocol/openid-connect/token

◦ Client ID: copsboot-mobile-client

◦ Client Secret: This is the secret that Keycloak has generated when you added the client to
Keycloak. Copy the value here.

Start the Spring Boot application.

45
Practical Guide to Building an API Back End with Spring Boot

Now, Postman will act as our "mobile application", get a token from Keycloak, and use that token to
access the protected resource on our resource server (e.g., our Spring Boot application).

Click on "Get new access token" in Postman. This will open your browser and show the login screen.

Now log in using the credentials of the user you created in Keycloak.

The login screen looks like you are logging into Keycloak. However, at the top, above
the login fields, it will show "COPSBOOT", so you can see you are loggin into your own
application.

In an actual production application, you will customize Keycloak to have a login page
styling that matches your brand/application.

After the login, your browser will open Postman again, and Postman will show the token it got from
Keycloak.

Now press "Use token" and click on the "Send" button to send a request to http://localhost:8080/api/
users/me using the token.

The response body should show something like this:

{
"subject": "eaa8b8a5-a264-48be-98de-d8b4ae2750ac",
"claims": {
"sub": "eaa8b8a5-a264-48be-98de-d8b4ae2750ac",
"resource_access": {
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"email_verified": false,
"allowed-origins": [
"https://oauth.pstmn.io"
],
"iss": "http://localhost:8180/realms/copsboot",
"typ": "Bearer",
"preferred_username": "[email protected]",
"given_name": "Wim",
"sid": "741a26a0-3a74-4f07-b709-3e26989091e0",
"aud": [
"account"
],

46
REST API Security

"acr": "1",
"realm_access": {
"roles": [
"default-roles-copsboot",
"offline_access",
"uma_authorization"
]
},
"azp": "copsboot-mobile-client",
"auth_time": 1694851053,
"scope": "email profile",
"name": "Wim Example",
"exp": "2023-09-16T08:02:37Z",
"session_state": "741a26a0-3a74-4f07-b709-3e26989091e0",
"iat": "2023-09-16T07:57:37Z",
"family_name": "Example",
"jti": "a4445824-c1ae-4050-8ea6-93e5c36aac91",
"email": "[email protected]"
}
}

We have now seen how to use Postman to get the token. If you are building your mobile application,
you will have to investigate how to do this same "OAuth 2.0 dance". Most likely, there are some well-
tested libraries to help you out.

If you use IntelliJ IDEA Ultimate, you can use the HTTP Client to test API calls like in
 Postman. The newest version now also has support for OAuth 2.0 authorization.

Testing
On the Spring Boot side, we don’t have that much code. We configured our security so any endpoint
under /api/ needs an authenticated user, and we configured the URL of Keycloak in
application.properties.

What we can test is that our /api/users/me endpoint is protected and only returns valid information
when using a valid JWT token.

This UserRestControllerTest will do just that:

package com.example.copsboot.user.web;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;

47
Practical Guide to Building an API Back End with Spring Boot

import static org.springframework.security.test.web.servlet.request


.SecurityMockMvcRequestPostProcessors.jwt;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserRestController.class) ①
class UserRestControllerTest {

@Autowired
private MockMvc mockMvc; ②

@Test
void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception
{
mockMvc.perform(get("/api/users/me")) ③
.andExpect(status().isUnauthorized()); ④
}

@Test
void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception {
mockMvc.perform(get("/api/users/me")
.with(jwt())) ⑤
.andExpect(status().isOk()) ⑥
.andExpect(jsonPath("subject").value("user")) ⑦
.andExpect(jsonPath("claims").isMap()) ⑧
.andDo(print()); ⑨
}
}

① Use WebMvcTest to have Spring Boot’s test slicing infrastructure setup anything that is needed to test
a RestController class.

② Inject the MockMvc instance which allows us to issue requests to the controller.

③ Perform a GET request on /api/users/me.

④ Validate that the 401 Unauthorized status code is returned since we are not passing any
authentication header when we do the request.

⑤ Pass a JWT token when the GET request is done.

⑥ Validate that we get a 200 Ok this time.

⑦ Check the JSON response to contain a field subject with the principal subject name.

⑧ There should also be a claims field with a map of all the claims.

48
REST API Security

⑨ This is optional, just to print out the request and response in case you want to have a look at what
exactly is happening in the test.

Summary
In this chapter, you added OAuth2 authentication to your application. You also created unit tests for it.
You learned about the differences between a Resource Server, an Authorization Server, and a client.

49
Practical Guide to Building an API Back End with Spring Boot

PART
SIX
Application Users

50
Application Users

Introduction
We have set up Keycloak in the previous chapter, which contains our application’s users. However, we
also have a user table in the database that our Spring Boot application uses. How can we connect the
two?

Each user in Keycloak is assigned a unique id. We can get that id at any endpoint in our application via
the @AuthenticationPrincipal Jwt jwt parameter like we did in UserRestController. So, if we also store
that unique id in our database, we can link the Keycloak user with our application user.

How we implement this depends on how the user registration flow works in our mobile application.
One way is to have Keycloak handle the user registration. Go to the Keycloak administration UI and
select 'Realm settings' > 'Login' > 'Login screen customization'. There, you can enable "User
registration".

If you now use Postman to get a new token, you will see a "Registration" link at the bottom of the login
page in your browser. Fill in all the fields, and a new user is created in Keycloak.

Another way would be to build a user registration screen in your mobile application. It would
probably use an endpoint in your Spring Boot application, which would, in turn, talk to Keycloak to
create the user. This flow makes it a bit easier to link the Keycloak user with the Spring Boot user, as
you have everything under your control. The drawback is that the user must type the password in the
mobile app UI, which is sent to the Spring Boot application before passing it onto Keycloak. Another
drawback is that such a user registration flow would not allow users to add social logins later, at least
not in an easy way.

Given the drawbacks of the second solution, let’s go with the first solution. The user will self-register
on the Keycloak website. Our mobile app should always start with a GET to /api/users/me. At that point,
query our Spring Boot database to see if the user already exists. If it does not, we will indicate this in
the result. If the user does not exist, the mobile app can issue a POST to /api/users to create the user.

Link Keycloak user to application user


Start by creating a value object to represent the id of the user on Keycloak:

package com.example.copsboot.user;

import org.springframework.util.Assert;

import java.util.UUID;

public record AuthServerId(UUID value) {


public AuthServerId {
Assert.notNull(value, "The AuthServerId value should not be null");

51
Practical Guide to Building an API Back End with Spring Boot

}
}

Update User.java to use that property and remove the password and roles fields, as these are stored in
Keycloak now. We then add a mobileToken field, which is a unique identifier that the mobile application
will send to be able to send push notifications to the device of the user:

package com.example.copsboot.user;

import com.example.orm.jpa.AbstractEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;

@Entity
@Table(name = "copsboot_user")
public class User extends AbstractEntity<UserId> {

private String email;


private AuthServerId authServerId; ①
private String mobileToken; ②

protected User() {

public User(UserId id, String email, AuthServerId authServerId, String mobileToken) {



super(id);
this.email = email;
this.authServerId = authServerId;
this.mobileToken = mobileToken;
}

public String getEmail() {


return email;
}

public AuthServerId getAuthServerId() { ④


return authServerId;
}

public String getMobileToken() {


return mobileToken;
}
}

52
Application Users

① Add AuthServerId field

② Add mobileToken field

③ Update constructor

④ Add getter

Update the /api/users/me endpoint to show the userId field if the user is known in our database:

Listing 13. com.example.copsboot.user.web.UserRestController

@GetMapping("/me") ①
public Map<String, Object> myself(@AuthenticationPrincipal Jwt jwt) { ②
Optional<User> userByAuthServerId = userService.findUserByAuthServerId(new
AuthServerId(UUID.fromString(jwt.getSubject())));

Map<String, Object> result = new HashMap<>();


userByAuthServerId.ifPresent(user -> result.put("userId", user.getId().
asString()));
result.put("subject", jwt.getSubject());
result.put("claims", jwt.getClaims());

return result;
}

To make this work, we need to inject the UserService into the UserRestController.

The code of the UserService itself is fairly straightforward:

package com.example.copsboot.user;

import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
public class UserService {
private final UserRepository repository; ①

public UserService(UserRepository repository) {


this.repository = repository;
}

public Optional<User> findUserByAuthServerId(AuthServerId authServerId) { ②


return repository.findByAuthServerId(authServerId);
}

53
Practical Guide to Building an API Back End with Spring Boot

① Inject the UserRepository

② Call the findByAuthServerId repository method.

We don’t need to write the implementation of findByAuthServerId on the repository as just naming the
method precisely that on the Spring Data repository will work:

public interface UserRepository extends CrudRepository<User, UserId>,


UserRepositoryCustom {
Optional<User> findByAuthServerId(AuthServerId authServerId);
}

We can now add the code to create a new user, based on the JWT token that we get. Add a PostMapping
to the UserRestController:

Listing 14. com.example.copsboot.user.web.UserRestController

@PostMapping
@ResponseStatus(HttpStatus.CREATED) ①
public UserDto createUser(@AuthenticationPrincipal Jwt jwt,
@RequestBody CreateUserRequest request) { ②
CreateUserParameters parameters = request.toParameters(jwt); ③
User user = userService.createUser(parameters);
return UserDto.fromUser(user); ④
}

① Set the response status code to 201 Created. This is the recommended status code if you create a
new entity.

② The mobile app will use a JSON request body to give us the mobile token. We map this JSON onto the
CreateUserRequest object.

③ Create an instance of CreateUserParameters, based on the request and the JWT token, which we give
to the service to create the new user.

④ Return the created user as a DTO (Data Transfer Object) back to the caller.

Why not return the User entity object?


It is perfectly possible to return the User object directly in the controller method. The
advantage of that is you don’t need to create an extra object with fields that closely
 resemble the entity. However, you most likely want to format the data a bit differently,
you want to hide some information, or you want to add extra information from
somewhere else.

54
Application Users

Changes are so much easier if you directly use a data transfer object (DTO) and avoid
annotating your entity with Jackson annotations, which can lead to more annotations
than code if you are not careful.

The CreateUserRequest is implemented as a record like this:

package com.example.copsboot.user.web;

import com.example.copsboot.user.AuthServerId;
import com.example.copsboot.user.CreateUserParameters;
import org.springframework.security.oauth2.jwt.Jwt;

import java.util.UUID;

public record CreateUserRequest(String mobileToken) { ①

public CreateUserParameters toParameters(Jwt jwt) {


AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject()));

String email = jwt.getClaimAsString("email"); ③
return new CreateUserParameters(authServerId, email, mobileToken);
}
}

① Record with the mobileToken field.

② The JWT token has the id that Keycloak assigns to a user as the subject. We convert this to our
AuthServerId value object here.

③ The email address of the user can be retrieved via the email claim in the token.

The CreateUserParameters object helps to define what parameters are needed to create the user. This is
how it looks:

package com.example.copsboot.user;

public record CreateUserParameters(AuthServerId authServerId, String email, String


mobileToken) {
}

We should not use the User object for example as we don’t have the UserId yet. This is something the
repository has to provide. By using such a parameters object, the responsibilities become a lot clearer.

In the UserService, the createUser method looks like this:

55
Practical Guide to Building an API Back End with Spring Boot

public User createUser(CreateUserParameters createUserParameters) {


UserId userId = repository.nextId();
User user = new User(userId, createUserParameters.email(),
createUserParameters.authServerId(),
createUserParameters.mobileToken());
return repository.save(user);
}

One last thing we need to do is to tell Hibernate how to persist the AuthServerId. As this is not a type it
understands by default, we need to do a little bit of extra work. This can be in the form of an
AttributeConverter or an Embeddable. See AttributeConverter vs Embeddable in JPA for more
information about the difference.

In this case, we are best with implementing an AttributeConverter:

package com.example.copsboot.user;

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;

import java.util.UUID;

@Converter(autoApply = true)
public class AuthServerIdAttributeConverter implements AttributeConverter<AuthServerId,
UUID> {
@Override
public UUID convertToDatabaseColumn(AuthServerId attribute) {
return attribute.value();
}

@Override
public AuthServerId convertToEntityAttribute(UUID dbData) {
return new AuthServerId(dbData);
}
}

You can also create a value object for mobileToken and email and create 2 additional
 AttributeConverters. If I were coding an actual production application, this is what I
would do.

If you start the application, you should be able to execute the following sequence:

1. Use Postman to do a GET on /api/users/me. The response will not include the userId field.

2. Now do a POST on /api/users with the following JSON body:

56
Application Users

{
"mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"
}

The response should show something like:

{
"userId": "790a23b5-42f6-4c81-98ec-e56d932f22b9",
"email": "[email protected]",
"authServerId": "eaa8b8a5-a264-48be-98de-d8b4ae2750ac",
"mobileToken": "c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"
}

3. Again, do a GET on /api/users/me. Now the response should show the userId field.

User roles
When using an authorization server such as Keycloak, user roles should be handled there.

Let’s add our 3 user roles in Keycloak. Go to 'Realm roles' in Keycloak and add the 3 roles via the
'Create Role' button:

• OFFICER

• CAPTAIN

• ADMIN

Via the 'Users' menu, we can assign a role to our test users. Select the 'Role mapping' tab for a certain
user and use the 'Assign role' button to assign the OFFICER role.

If you do a GET on /api/users/me now, you will see that the role is present in the claims:

{
"subject": "eaa8b8a5-a264-48be-98de-d8b4ae2750ac",
"claims": {
...
"realm_access": {
"roles": [
"default-roles-copsboot",
"offline_access",
"OFFICER",
"uma_authorization"
]

57
Practical Guide to Building an API Back End with Spring Boot

},
...
}

We can update our creation endpoint to only allow creating an application user if the OFFICER role is
present.

To make it easy to map the Keycloak roles to Spring Security roles, we will use the Spring Addons
library. Add it to your pom.xml:

<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-starter-oidc</artifactId>
<version>7.1.9</version>
</dependency>

Configure it via application.properties:

com.c4-soft.springaddons.oidc.ops[0].iss=http://localhost:8180/realms/copsboot
com.c4-soft.springaddons.oidc.ops[0].authorities[0].path=$.realm_access.roles
com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix=ROLE_

The important part is the path and prefix properties.

• The path property indicates the JSON path expression to access the list of roles in the JWT claims.

• Spring Security assumes a default prefix of ROLE_. By setting the prefix property to the same value,
we can use an expression like hasRole('OFFICER') in our Java code. Without this property, we would
need to use ROLE_OFFICER as the role name in Keycloak (or do more advanced customization in our
Spring Boot application).

We can remove spring.security.oauth2.resourceserver.jwt.issuer-uri from


 application.properties
soft.springaddons.oidc.ops.
as this is now configured via com.c4-

The final bit to adjust is the WebSecurityConfiguration. Instead of defining our own SecurityFilterChain,
we will now use the one that the spring-addons library provides. We just need to copy over the part
where we define the security for our routes:

package com.example.copsboot.infrastructure.security;

import
com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServer
ExpressionInterceptUrlRegistryPostProcessor;

58
Application Users

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import
org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;

@Configuration
@EnableMethodSecurity ①
public class WebSecurityConfiguration {

@Bean
ResourceServerExpressionInterceptUrlRegistryPostProcessor authorizePostProcessor() {

return registry -> registry.requestMatchers(HttpMethod.OPTIONS, "/api/**"
).permitAll()
.requestMatchers("/api/**").authenticated()
.anyRequest().authenticated();
}
}

① Enable method security so we can use @PreAuthorize in our controller.

② Declare a ResourceServerExpressionInterceptUrlRegistryPostProcessor bean to override the default


that spring-addons provides.

The spring-addons library also allows to configure this via certain properties, but I
 like to have this part in Java.

If you want to keep using your SecurityFilterChain bean, then you can still use the
role mapping from the properties if you do it like this:

@Bean
SecurityFilterChain configureSecurityFilterChain(HttpSecurity http,
Converter<Jwt,
AbstractAuthenticationToken> authenticationConverter) throws Exception {

 http
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
.requestMatchers(HttpMethod.OPTIONS, "/api/**"
).permitAll()
.requestMatchers("/api/**").authenticated()
.anyRequest().authenticated())
.oauth2ResourceServer(resourceServer -> resourceServer.
jwt( jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter

59
Practical Guide to Building an API Back End with Spring Boot

(authenticationConverter))); ②

return http.build();
}

There are some other things that spring-addons defines by default that are not applied
in this example, such as the stateless session and the csrf protection that is disabled.
Take a look at SpringAddonsOidcResourceServerBeans to see all the defaults.

Now we can adjust the endpoint to only allow users with the OFFICER role to call the endpoint:

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@PreAuthorize("hasRole('OFFICER')") ①
public UserDto createUser(@AuthenticationPrincipal Jwt jwt,
@RequestBody CreateUserRequest request) {
CreateUserParameters parameters = request.toParameters(jwt);
User user = userService.createUser(parameters);
return UserDto.fromUser(user);
}

① Use the hasRole expression to only allow users with the OFFICER role to call this endpoint.

If you restart your application and test again, you should be able to do a POST on /api/users from
Postman, but only if the user is assigned the OFFICER role in Keycloak. If not, a 403 Forbidden will be
returned.

Update tests

Because UserRestController now depends on UserService, our UserRestControllerTest is now broken.


Let’s fix this and also add a test for the creation of a user.

Add the spring-addons test module as a dependency:

<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-starter-oidc-test</artifactId>
<version>7.1.9</version>
</dependency>

This is the updated test:

package com.example.copsboot.user.web;

60
Application Users

import
com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceSer
verSecurity;
import com.example.copsboot.infrastructure.security.WebSecurityConfiguration;
import com.example.copsboot.user.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.test.web.servlet.MockMvc;

import java.util.UUID;

import static org.mockito.ArgumentMatchers.any;


import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.servlet.request
.SecurityMockMvcRequestPostProcessors.jwt;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserRestController.class)
@AutoConfigureAddonsWebmvcResourceServerSecurity ①
@Import(WebSecurityConfiguration.class) ②
class UserRestControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private UserService userService; ③

@Test
void givenUnauthenticatedUser_userInfoEndpointReturnsUnauthorized() throws Exception
{
mockMvc.perform(get("/api/users/me"))
.andExpect(status().isUnauthorized());
}

@Test
void givenAuthenticatedUser_userInfoEndpointReturnsOk() throws Exception {
String subject = UUID.randomUUID().toString(); ④

61
Practical Guide to Building an API Back End with Spring Boot

mockMvc.perform(get("/api/users/me")
.with(jwt().jwt(builder -> builder.subject(subject)))) ⑤
.andExpect(status().isOk())
.andExpect(jsonPath("subject").value(subject)) ⑥
.andExpect(jsonPath("claims").isMap());
}

@Test
void givenAuthenticatedOfficer_userIsCreated() throws Exception { ⑦
UserId userId = new UserId(UUID.randomUUID());
when(userService.createUser(any(CreateUserParameters.class)))
.thenReturn(new User(userId,
"[email protected]",
new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-
d8b4ae2750ac")),

"c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"));
mockMvc.perform(post("/api/users")
.with(jwt().jwt(builder -> builder.subject(UUID.randomUUID
().toString()))
.authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"mobileToken":
"c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"
}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("userId").value(userId.asString()))
.andExpect(jsonPath("email").value("[email protected]"))
.andExpect(jsonPath("authServerId").value("eaa8b8a5-a264-48be-98de-
d8b4ae2750ac"));
}

@Test
void givenAuthenticatedUserThatIsNotAnOfficer_forbiddenIsReturned() throws Exception
{
mockMvc.perform(post("/api/users")
.with(jwt().jwt(builder -> builder.subject(UUID.randomUUID
().toString())))
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"mobileToken":
"c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"
}

62
Application Users

"""))
.andExpect(status().isForbidden()); ⑧
}
}

① Ensure the auto-configuration of spring-addons is applied in the test.

② Load our security configuration so the test behaves just as our application.

③ Inject a mock instance of the UserService using @MockBean.

④ Generate a subject as a UUID so the JWT token will have a UUID subject instead of the default user
subject we had before.

⑤ Customize the JWT token generation to use the subject we want.

⑥ Validate the value of the subject in the response.

⑦ Test for the POST request where we assign the ROLE_OFFICIER authority.

We use the .authorities() method in the test to give the JWT token a role of OFFICER.
An alternative is to add a @WithJwt annotation on the test method itself.

Follow these steps if you want to do this:

1. Extract a token from Keycloak. The easiest is to use Postman to do a call and copy
the access token into the https://jwt.io website to decode it.

2. Put the decoded JSON into a file (e.g., jwt-officer.json) in the src/main/resources
directory:

Listing 15. jwt-officer.json

{
"exp": 1694870234,
 "iat": 1694869934,
"auth_time": 1694865932,
"jti": "7b933105-60b9-43ae-8725-a34bff521858",
"iss": "http://localhost:8180/realms/copsboot",
"aud": "account",
"sub": "eaa8b8a5-a264-48be-98de-d8b4ae2750ac",
"typ": "Bearer",
"azp": "copsboot-mobile-client",
"session_state": "2866bba7-d53f-498e-8830-4dcf0bcb865e",
"acr": "0",
"allowed-origins": [
"https://oauth.pstmn.io"
],
"realm_access": {
"roles": [

63
Practical Guide to Building an API Back End with Spring Boot

"default-roles-copsboot",
"offline_access",
"OFFICER",
"uma_authorization"
]
},
"resource_access": {
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "email profile",
"sid": "2866bba7-d53f-498e-8830-4dcf0bcb865e",
"email_verified": false,
"name": "Wim Example",
"preferred_username": "[email protected]",
"given_name": "Wim",
"family_name": "Example",
"email": "[email protected]"
}

3. Annotate the test with @WithJwt:

@Test
@WithJwt("jwt-officer.json")
void givenAuthenticatedOfficer_userIsCreated() throws Exception {

UserId userId = new UserId(UUID.randomUUID());
when(userService.createUser(any(CreateUserParameters.class)))
.thenReturn(new User(userId,
"[email protected]",
new AuthServerId(UUID.fromString("eaa8b8a5-
a264-48be-98de-d8b4ae2750ac"))));
mockMvc.perform(post("/api/users"))
.andExpect(status().isCreated())
.andExpect(jsonPath("userId").value(userId.asString()))
.andExpect(jsonPath("email").value("[email protected]"))
.andExpect(jsonPath("authServerId").value("eaa8b8a5-
a264-48be-98de-d8b4ae2750ac"));
}

64
Application Users

If you want to run the CopsbootApplicationTests integration test (Or run all tests via
 Maven), then you need to have Keycloak running otherwise the test will fail.

Writing API documentation


You now have two API endpoints and need to provide documentation so that clients who want to use
these endpoints know what to expect from your API. There are various options for documentation
(e.g., Swagger or Postman) but I really like what Spring REST Docs offers in this regard.

Spring REST Docs allows you to generate AsciiDoc-formatted snippets that you can include in
documentation you write in Asciidoctor. You can generate these snippets by running unit tests with
Spring MVC Test.

Your documentation is always up to date, since the JSON in the documentation is the actual JSON that
your code produces. You can explain the API in your documentation and include those current JSON
snippets as part of the final HTML or PDF document.

Maven setup

The first thing you need to add is the spring-restdocs-mockmvc dependency so that you can write unit
tests using Mock MVC to generate the JSON snippets:

<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>

To set up the HTML and PDF output generation, you need to configure the asciidoctor-maven-plugin
(you can leave out one or the other if you want only HTML or only PDF output):

Listing 16. pom.xml

<pluginManagement>
<plugins>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>

65
Practical Guide to Building an API Back End with Spring Boot

</goals>
<configuration>
<backend>html</backend>
</configuration>
</execution>
<execution>
<id>generate-docs-pdf</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>pdf</backend>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-asciidoctor</artifactId>
<version>${spring-restdocs.version}</version>
</dependency>
<dependency>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctorj-pdf</artifactId>
<version>2.3.9</version>
</dependency>
</dependencies>
<configuration>
<doctype>book</doctype>
<attributes>
<project-version>${project.version}</project-version>
</attributes>
</configuration>
</plugin>
</plugins>
</pluginManagement>

We are adding this in the pluginManagement section of Maven, which mean the plugin is configured, but
not active yet. This is deliberate as you only want to generate the documentation when running with a
certain Maven profile. In this case, I am calling this profile ci, as it will be run on our continuous-
integration server:

<profiles>
<profile>

66
Application Users

<id>ci</id>
<build>
<plugins>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</profile>
</profiles>

By binding the process-asciidoc goal to the prepare-package, you ensure that the documentation is built
whenever somebody runs mvn package -Pci (-P allows you to activate a Maven profile via the
command line).

It might seem that using a Maven profile just to generate documentation is a bit of
overkill at this point, but you are doing this now to prepare for including more build
 steps that should only be done on the build server like code coverage and static code
analysis.

Asciidoctor document

The Maven setup now allows you to create HTML and PDF output, but of course you need a source file.
To get started, create Copsboot REST API Guide.adoc in the src/docs/asciidoc folder:

Listing 17. Copsboot REST API Guide.adoc

= Copsboot REST API Guide


:icons: font
:toc:
:toclevels: 2

:numbered:

== Introduction

The Copsboot project uses a REST API for interfacing with the server.

This documentation covers version {project-version} of the application.

Don’t worry about the AsciiDoc syntax for now. If you want, you can read the AsciiDoc Syntax Quick
Reference or the Asciidoctor Documentation for more information.

Notice how you have {project-version} in the source document. If you now run mvn package -Pci on

67
Practical Guide to Building an API Back End with Spring Boot

the command line, you will get the HTML and PDF output in the target/generated-docs folder. There,
{project-version} is replaced with 0.0.1-SNAPSHOT, which is the current version of your Maven project.

Figure 2. Copsboot REST API Guide.pdf

This works because you have made the Maven project version available to Asciidoctor in the Maven
configuration of the plugin:

<attributes>
<project-version>${project.version}</project-version>
</attributes>

The name of the XML element is the name of the variable to be used in the Asciidoctor document. The
text inside the element is the value, for which this example uses the built-in Maven variable
project.version.

This ensures that you always know what version of the software the documentation refers to.

UserRestController documentation

With all of that out of the way, you can create the unit test that will generate the JSON snippets for your
documentation. First, create a test class called UserRestControllerDocumentation. The class annotations
and the fields you have are almost the same as those you already have in UserRestControllerTest.

Listing 18. UserRestControllerDocumentation.java

@WebMvcTest(UserRestController.class)
@AutoConfigureAddonsWebmvcResourceServerSecurity
@Import(WebSecurityConfiguration.class)
@ExtendWith(RestDocumentationExtension.class)
@AutoConfigureRestDocs
public class UserRestControllerDocumentation {

@Autowired
private MockMvc mockMvc;

@MockBean
private UserService service;

68
Application Users

There are the following differences between this and UserRestControllerTest:

1. We add the JUnit extension RestDocumentationExtension to automatically configure the output path
where the snippets are generated.

2. The annotation @AutoConfigureRestDocs to configure the MockMvc instance for the documentation
generation.

The final piece of configuration is done via a @TestConfiguration inner class:

Listing 19. UserRestControllerDocumentation.java

@TestConfiguration
static class TestConfig {
@Bean
public RestDocsMockMvcConfigurationCustomizer
restDocsMockMvcConfigurationCustomizer() {
return configurer -> configurer.operationPreprocessors()
.withRequestDefaults(prettyPrint())
.withResponseDefaults(prettyPrint(),
modifyHeaders().removeMatching("X.*")
.removeMatching("Pragma")
.removeMatching("Expires"));
}
}

An inner class annotated with @TestConfiguration is automatically loaded by the test. We have 1 bean
defined here to customize how the request and responses that are generated in the unit test are
written to the snippets. We pretty print the JSON so it looks nice in our documentation and we also
remove some caching headers that we don’t need to make visible in the documentation.

Documentation of GET

With this in place, you can write your first documentation test:

Listing 20. UserRestControllerDocumentation.java

@Test
public void ownUserDetailsWhenNotLoggedInExample() throws Exception {
mockMvc.perform(get("/api/users/me"))
.andExpect(status().isUnauthorized())
.andDo(document("own-details-unauthorized"));
}

This basically is just a simple unit test. The only difference is that you import
org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get instead of

69
Practical Guide to Building an API Back End with Spring Boot

org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get.

To actually generate the documentation, we add andDo(document("own-details-unauthorized")).

Try running this test. It should generate a directory called own-details-unauthorized in


target/generated-snippets with six files inside it:

• curl-request.adoc,

• http-request.adoc,

• http-response.adoc,

• httpie-request.adoc,

• request-body.adoc, and

• response-body.adoc.

The most important file for your current test is http-response.adoc as this shows the failure response to
be expected when calling /api/users/me if you are not authenticated:

[source,http,options="nowrap"]
----
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="Restricted Content"
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY

----

You can now include this snippet of Asciidoctor in your API documentation. To split the documentation
into small pieces, create the _users.adoc file with this content:

Listing 21. _users.adoc

== User Management

=== User information

The API allows to get information on the currently logged on user


via a `GET` on `/api/users/me`. If you are not a logged on user, the
following response will be returned:

70
Application Users

operation::own-details-unauthorized[snippets='http-request,http-response']

Notice the operation:: line at the end of your document. It has two important parts:

1. The part between the :: and the [ (own-user-details-unauthorized in this example) needs to match
the name of the generated directory in the target/generated-snippets folder.

2. The snippets='http-request,http-response' part defines which of the files in the folder should be
included in your documentation. The names match the file names, minus the .adoc extension.

Start the include file’s name with an underscore (_) so that the AsciiDoc Maven plugin
 will not try to create a separate output file for the include file.

Now update Copsboot REST API Guide.adoc to include the new _users.adoc file:

include::_users.adoc[]

Because our test ends with Documentation, we need to configure surefire to also pick it up (By default,
only classes ending with Test are considered):

Listing 22. pom.xml

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
<printSummary>false</printSummary>
<includes>
<include>**/*.java</include>
</includes>
</configuration>
</plugin>

If we now run mvn clean package -Pci, we get the following PDF generated:

71
Practical Guide to Building an API Back End with Spring Boot

Documentation of GET with response fields

Your first documentation test could remain simple because there was no JSON response to document.
The 401 Unauthorized status code was the most important part.

Now, expand your documentation with a case that involves response fields. Here is a test for an
authenticated user with the OFFICER role:

Listing 23. UserRestControllerDocumentation.java

@Test
public void authenticatedOfficerDetailsExample() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me")
.with(jwt().jwt(builder -> builder.subject(UUID.randomUUID
().toString()))
.authorities(new SimpleGrantedAuthority(
"ROLE_OFFICER"))))
.andExpect(status().isOk())
.andDo(document("own-details",
responseFields(
fieldWithPath("subject").description("The subject from
the JWT token"),
subsectionWithPath("claims").description("The claims from
the JWT token")
)));
}

Using the static methods responseFields, fieldWithPath, and subsectionWithPath, document the reponse
fields in the response body. Using fieldWithPath not only instructs Spring REST Docs to output a table

72
Application Users

with the name and description of each field, but also checks if the fields are really present in the
returned JSON. The subsectionWithPath method can be used to document a JSON object as a whole,
without the need to specify each field individually. Spring REST Docs will also fail the unit test if there
are fields in the response that are not documented.

With this added, the test generates a new snippet called response-fields.adoc, which looks like this:

|===
|Path|Type|Description

|`+subject+`
|`+String+`
|The subject from the JWT token

|`+claims+`
|`+Object+`
|The claims from the JWT token

|===

This is an AsciiDoc table to which you can add to your _users.doc. You can, for example, add something
like this:

If you do log on as a user, you get more information on that user:

operation::own-details[snippets='http-request,http-response,response-fields']

Note how we added response-fields as a third item in the snippets parameter. This results in the
following output (with a HTML example for a change):

73
Practical Guide to Building an API Back End with Spring Boot

Figure 3. Copsboot REST API Guide.html

Documentation of POST with response fields

As a final example of writing documentation for a REST API, you will document the POST call to create
a new user account. Remember, this is how the UserRestController defines the POST method:

Listing 24. UserRestController.java

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@PreAuthorize("hasRole('OFFICER')")
public UserDto createUser(@AuthenticationPrincipal Jwt jwt,
@RequestBody CreateUserRequest request) {
CreateUserParameters parameters = request.toParameters(jwt);
User user = userService.createUser(parameters);
return UserDto.fromUser(user);
}

In this case, you need to document the request body as well as the response body. The test method that

74
Application Users

will generate the documentation snippets looks like this:

Listing 25. UserRestControllerDocumentation.java

@Test
public void createOfficerExample() throws Exception {
UserId userId = new UserId(UUID.randomUUID());
when(service.createUser(any(CreateUserParameters.class)))
.thenReturn(new User(userId,
"[email protected]",
new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-98de-
d8b4ae2750ac")),

"c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"));
mockMvc.perform(post("/api/users")
.with(jwt().jwt(builder -> builder.subject(UUID.randomUUID
().toString()))
.authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"mobileToken":
"c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"
}
"""))
.andExpect(status().isCreated())
.andDo(document("create-user",
requestFields( ①
fieldWithPath("mobileToken")
.description("The unique mobile token of the
device (for push notifications).")
),
responseFields( ②
fieldWithPath("userId")
.description("The unique id of the user."),
fieldWithPath("email")
.description("The email address of the user."),
fieldWithPath("authServerId")
.description("The id of the user on the
authorization server."),
fieldWithPath("mobileToken")
.description("The unique mobile token of the
device (for push notifications).")
)));
}

75
Practical Guide to Building an API Back End with Spring Boot

① Document the request field.

② Doument the response fields.

With this test in place, update your _user.adoc documentation file again:

Listing 26. _users.adoc

=== Create a user

To create an new user, do a `POST` on `/api/users`:

operation::create-user[snippets='http-request,request-fields,http-response,response-
fields']

Note how you added request-fields to the snippets parameter.

The resulting PDF looks like this:

76
Application Users

If you’d like to add more text in your documentation for each of the snippets, you can
include individual snippets one by one by using the include macro instead of the
operation macro.

 For example:

77
Practical Guide to Building an API Back End with Spring Boot

The request fields for the create user call are documented in this table:

include::{snippets}/create-user/request-fields.adoc[]

The snippets variable is automatically resolved to the location where the snippets are
generated.

Refactoring to avoid duplication


So far, you have built a unit test and some nice documentation for your RestController. However, there
is some duplication that you should avoid before it gets out of hand:

• Both UserRestControllerTest and UserRestControllerDocumentation use @WebMvcTest to enable the


web test slicing.

• Both use @AutoConfigureAddonsWebmvcResourceServerSecurity and


@Import(WebSecurityConfiguration.class) to have the proper security setup for testing.

If you were to create other unit tests and documentation tests for other controllers, you would also
duplicate all that there. Luckily, Spring supports meta-annotations so that you can compose your own
annotation with defaults like you want them.

The first step is creating your own annotation:

@Retention(RetentionPolicy.RUNTIME) ①
@WebMvcTest ②
@AutoConfigureAddonsWebmvcResourceServerSecurity ③
@Import(WebSecurityConfiguration.class) ④
public @interface CopsbootControllerTest {

@AliasFor(annotation = WebMvcTest.class, attribute = "value") ⑤


Class<?>[] value() default {};

@AliasFor(annotation = WebMvcTest.class, attribute = "controllers") ⑥


Class<?>[] controllers() default {};
}

① Set the retention of the annotation to RUNTIME so that Spring can "view" the annotation.

② Add @WebMvcTest so that your own annotation gets all the magic of that annotation as well.

③ Enable autoconfiguration for security testing.

④ Import our security configuration.

⑤ Allow the user of your annotation to pass in the controller under test and pass it in your turn to

78
Application Users

@WebMvcTest.

⑥ Allow the user of your annotation to also use controllers as parameter (next to value as they are
just aliases of each other).

You can now update your UserControllerTest from:

@WebMvcTest(UserRestController.class)
@AutoConfigureAddonsWebmvcResourceServerSecurity
@Import(WebSecurityConfiguration.class)
class UserRestControllerTest {

to:

@CopsbootControllerTest(UserRestController.class)
class UserRestControllerTest {

In the same way, you can update UserRestControllerDocumentation to:

@CopsbootControllerTest(UserRestController.class)
@ExtendWith(RestDocumentationExtension.class)
@AutoConfigureRestDocs
public class UserRestControllerDocumentation {

Going one step further, we can also create a dedicated annotation for our documentation tests:

@Retention(RetentionPolicy.RUNTIME)
@CopsbootControllerTest
@ExtendWith(RestDocumentationExtension.class)
@AutoConfigureRestDocs
@ContextConfiguration(classes = CopsbootControllerDocumentationTestConfiguration.class)
public @interface CopsbootControllerDocumentationTest {

@AliasFor(annotation = WebMvcTest.class, attribute = "value") ⑤


Class<?>[] value() default {};

@AliasFor(annotation = WebMvcTest.class, attribute = "controllers") ⑥


Class<?>[] controllers() default {};
}

We converted the inner class from UserRestControllerDocumentation into the external


CopsbootControllerDocumentationTestConfiguration class and use it via the @ContextConfiguration

79
Practical Guide to Building an API Back End with Spring Boot

annotation.

With this new annotation, the documentation test can just be annotated with a single annotation and
the inner class can be removed:

@CopsbootControllerDocumentationTest(UserRestController.class)
public class UserRestControllerDocumentation {

Summary
You have seen how to add GET and POST endpoints to your application and how to map the JSON that
is sent to a Java class for further processing in your application.

You have written unit tests and documentation for those endpoints.

This concludes your first encounter with the REST API. It’s time to focus on getting a "real" database
working.

80
Working with a Real Database

PART
SEVEN
Working with a Real Database

81
Practical Guide to Building an API Back End with Spring Boot

So far, you have been developing with H2, an in-memory database. If you want to take your
application to production, you need to use a "real" database like MySQL, PostgreSQL, or Microsoft SQL
Server.

For the sake of this book, we’re assuming that the database is relational and not a
 NoSQL database, although Spring Data supports many NoSQL databases.

For this example, we will use PostgreSQL, an open-source database, as it is free to use and runs on all
major operating systems. It also natively supports UUID columns, which is why I prefer it over MySQL.

Installation of PostgreSQL
It’s easy to install PostgreSQL through Docker so let’s do that. Using the official Postgres Docker
repository, we can add it to our docker-compose.yaml file:

version: '3'
services:
db:
image: 'postgres:16.0'
ports:
- 5432:5432
environment:
POSTGRES_PASSWORD: my-postgres-db-pwd
identity:
image: 'quay.io/keycloak/keycloak:22.0.1'
entrypoint: /opt/keycloak/bin/kc.sh start-dev --import-realm
ports:
- '8180:8080'
environment:
KEYCLOAK_LOGLEVEL: 'INFO'
KEYCLOAK_ADMIN: 'admin'
KEYCLOAK_ADMIN_PASSWORD: 'admin-secret'
KC_HOSTNAME: 'localhost'
KC_HEALTH_ENABLED: 'true'
KC_METRICS_ENABLED: 'true'

Run docker-compose up -d to start the database (and Keycloak we had defined before)

Running docker ps should show that the containers are running:

Wims-MacBook-Pro:~ wdb$ docker ps


CONTAINER ID IMAGE COMMAND CREATED
STATUS PORTS NAMES
589f170e5827 postgres:16.0 "docker-entrypoint.s…" 2 minutes ago

82
Working with a Real Database

Up 2 minutes 0.0.0.0:5432->5432/tcp, :::5432->5432/tcp copsboot-db-1


6c82fb603d05 quay.io/keycloak/keycloak:22.0.1 "/opt/keycloak/bin/k…" 2 minutes ago
Up 2 minutes 8443/tcp, 0.0.0.0:8180->8080/tcp, :::8180->8080/tcp copsboot-identity-1

If you want to access the database via command line, you can do this via docker exec -it copsboot-db-
1 bash.

This command brings you into the Docker container that runs PostgreSQL. You can now start the client
with psql -U postgres.

Here, you can create the database: CREATE DATABASE copsbootdb;.

Exit from the PostgreSQL client: \q.

And exit from the Docker container: exit.

Using PostgreSQL
Starting up with the local PostgreSQL

The best way to select which database to connect to is with Spring Profiles. You will use a profile that
you’ll call local to connect to your local database. The first thing you need to do is create an
application-local.properties file to hold your connection details.

Listing 27. application-local.properties

spring.datasource.url=jdbc:postgresql://localhost/copsbootdb ①
spring.datasource.driverClassName=org.postgresql.Driver ②
spring.datasource.username=postgres ③
spring.datasource.password=my-postgres-db-pwd ④
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect ⑤
spring.jpa.hibernate.ddl-auto=validate ⑥

① The JDBC URL defines the kind of database (postgresql), where it is running (localhost), and the
name of the database (copsbootdb).

② This is the JDBC driver to use.

③ This is the user that has the appropriate rights on the database.

④ This is the password of the user.

⑤ This is the dialect that our JPA implementation (in our case, Hibernate) should use.

⑥ Disable the automatic creation of the database tables by setting this property to validate. (You can
also use none if you want to disable the validation.)

 This file should never be added to version control as it contains your personal setup

83
Practical Guide to Building an API Back End with Spring Boot

and password. A good practice is committing an application-


local.properties.template so there is a sample file to start from.

If you are using git, create a .gitignore file and add application-local.properties to it
like this:

Listing 28. .gitignore

/src/main/resources/application-local.properties

Next, you need to update your pom.xml to include the PostgreSQL driver:

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

Also remove the h2 dependency from the pom.xml as we won’t be using it anymore.

Database table creation

If you start the application and try creating a user, you will get errors telling you that you have no
tables in your database. With PostgreSQL, this message looks like this:

org.postgresql.util.PSQLException: ERROR: relation "copsboot_user" does not exist

Spring Boot supports two libraries that allow SQL scripts to run automatically at startup to ensure that
your database is up to date: Flyway and Liquibase.

The biggest difference between the two is that Flyway uses SQL scripts while Liquibase uses XML
syntax. This project will use Flyway but Liquibase is an equally valid choice. JHipster, for example,
uses Liquibase.

Start by adding the dependency in the pom.xml:

<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>

Now, create a db/migration directory inside src/main/resources as this is the default location for Flyway.

84
Working with a Real Database

Inside that directory, we add the Flyway migration scripts for PostgreSQL:

pom.xml
mvnw
mvnw.cmd
src
|-- main
|-- java
|-- com.springbook.application
|-- Application
|-- resources
|-- application.properties
|-- db
|-- migration
|-- postgresql
|-- V1.0.0.1__users.sql ①
|-- test
|-- java
|-- com.springbook.application
|-- ApplicationTests

① This is the migration script to create the copsboot_user table for PostgreSQL.

The name of the script itself is important as Flyway will use that to determine the order in which the
scripts are run. See Versioned Migrations at the Flyway site for more on that.

Generate DDL scripts

You can manually write those SQL scripts for Flyway, but you can also get a little help from Spring
Boot.

Add the following properties to application-local.properties:

spring.jpa.properties.jakarta.persistence.schema-generation.create-source=metadata
spring.jpa.properties.jakarta.persistence.schema-generation.scripts.action=create
spring.jpa.properties.jakarta.persistence.schema-generation.scripts.create-
target=create.sql
spring.jpa.properties.hibernate.hbm2ddl.delimiter=;

Now start the application with the local profile active. Spring Boot will create a file called create.sql
that contains creation scripts for all entities in your application:

create table copsboot_user (auth_server_id uuid, id uuid not null, email varchar(255),

85
Practical Guide to Building an API Back End with Spring Boot

mobile_token varchar(255), primary key (id));

If your application does not start because there are entities for which there is no
database table yet, you can temporarily set spring.jpa.hibernate.ddl-auto to create-
 drop. Once you have the create.sql file to serve as a basis for your migration file,
change spring.jpa.hibernate.ddl-auto back to validate.

After copying this to your migration file and some reformatting, you end up with this migration file for
the users table:

Listing 29. V1.0.0.1__users.sql

CREATE TABLE copsboot_user


(
id uuid NOT NULL PRIMARY KEY,
auth_server_id uuid,
email VARCHAR(255),
mobile_token VARCHAR(255)
);

Updates
Flyway will store a flyway_schema_history table in your database so it knows which migrations have
already run and which have not. It also stores a checksum of the migration file that was used.

It is crucial never to update a migration file in your project once you have released this migration to
production because Flyway will notice that the checksum no longer matches and will refuse to run
anything anymore. This will effectively prevent the startup of your application.

During development, you can drop all tables and perform the migrations again. But in production, that
would be a terrible idea. Once you have released version 1.0 for example, you should not change
anything in the migration scripts that already exist. If you want to remove a column or change a
column type, create an additional migration file that applies those changes.

Integration testing
H2 is great to get started easily, but nothing like using the actual database if possible. Luckily, we have
Testcontainers now, so we can use the real PostgreSQL running in Docker during our tests.

To get started, add the dependencies in pom.xml:

<dependency>
<groupId>org.springframework.boot</groupId>

86
Working with a Real Database

<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>

Use a new Spring Profile repository-test to connect to the Testcontainers-managed PostgreSQL


instance. For this, create a new file application-repository-test.properties in the src/test/resources
folder:

Listing 30. application-integration-test.properties

spring.datasource.url=jdbc:tc:postgresql:16://localhost/copsbootdb
spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.username=user
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=validate

Here are some points of interest:

• spring.datasource.url uses the tc prefix. The hostname (localhost) and database name (copsbootdb)
don’t matter; Testcontainers ignores them.

• spring.datasource.driverClassName uses the Testcontainers JDBC driver. This driver will start a
Docker instance with your PostgreSQL database.

• spring.datasource.username is always user and spring.datasource.password is always password.

• spring.jpa.hibernate.ddl-auto is set to validate since you want Flyway to create the tables.

See JDBC URL for more on different options. For instance, you can choose a specific

 version like this:

jdbc:tc:mysql:5.7.34://somehostname:someport/databasename

With this in place, you can now update the UserRepositoryTest to a test that will use PostgreSQL:

package com.example.copsboot.user;

87
Practical Guide to Building an API Back End with Spring Boot

import com.example.copsboot.infrastructure.SpringProfiles;
import com.example.orm.jpa.InMemoryUniqueIdGenerator;
import com.example.orm.jpa.UniqueIdGenerator;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;

import java.util.HashSet;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) ①
@ActiveProfiles(SpringProfiles.REPOSITORY_TEST) ②
public class UserRepositoryTest {

@Autowired
private UserRepository repository;
@PersistenceContext
private EntityManager entityManager;
@Autowired
private JdbcTemplate jdbcTemplate;

@Test
public void testStoreUser() {
User user = repository.save(new User(repository.nextId(),
"[email protected]",
new AuthServerId(UUID.randomUUID()),
"c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"));
assertThat(user).isNotNull();

assertThat(repository.count()).isEqualTo(1L);

entityManager.flush(); ③

assertThat(jdbcTemplate.queryForObject("SELECT count(*) FROM copsboot_user",

88
Working with a Real Database

Long.class)).isEqualTo(1L); ④
assertThat(jdbcTemplate.queryForObject("SELECT email FROM copsboot_user",
String.class)).isEqualTo("[email protected]");
}

@TestConfiguration
static class TestConfig {
@Bean
public UniqueIdGenerator<UUID> generator() {
return new InMemoryUniqueIdGenerator();
}
}
}

① Configure the testing framework to not replace the database with a test database (since you want to
use your Testcontainer database).

② Activate the repository-test profile so that the testing framework starts and uses the Testcontainer
PostgreSQL database.

③ Make the JPA framework flush all changes to the database so you can inspect the database tables.

④ Run some JDBC queries to validate if the user was properly saved.

Running the test will show output similar to this:

2023-09-20 14:50:56 INFO [main] DockerClientProviderStrategy - Loaded


org.testcontainers.dockerclient.UnixSocketClientProviderStrategy from
~/.testcontainers.properties, will try it first
2023-09-20 14:50:56 WARN [main] DockerClientProviderStrategy - DOCKER_HOST
tcp://127.0.0.1:55605 is not listening
2023-09-20 14:50:56 INFO [main] DockerClientProviderStrategy - Found Docker environment
with local Unix socket (unix:///var/run/docker.sock)
2023-09-20 14:50:56 INFO [main] DockerClientFactory - Docker host IP address is localhost
2023-09-20 14:50:56 INFO [main] DockerClientFactory - Connected to docker:
Server Version: 24.0.6
API Version: 1.43
Operating System: OrbStack
Total Memory: 3916 MB
2023-09-20 14:50:57 INFO [main] postgres:16 - Creating container for image: postgres:16
2023-09-20 14:50:57 INFO [main] RegistryAuthLocator - Credential helper/store (docker-
credential-desktop) does not have credentials for https://index.docker.io/v1/
2023-09-20 14:50:57 INFO [main] 1 - Creating container for image:
testcontainers/ryuk:0.5.1
2023-09-20 14:50:57 INFO [main] 1 - Container testcontainers/ryuk:0.5.1 is starting:
f8a79b5d608bda7b1e9a0c4b040a2c28eaec6e58c6547cf2c4c5a1033b49a018
2023-09-20 14:50:57 INFO [main] 1 - Container testcontainers/ryuk:0.5.1 started in

89
Practical Guide to Building an API Back End with Spring Boot

PT0.356408S
2023-09-20 14:50:57 INFO [main] postgres:16 - Container postgres:16 is starting:
6f0ba87880832ad97930acbc02ea535379d3ee3703a874f986eadaba786b47ca
2023-09-20 14:50:59 INFO [main] postgres:16 - Container postgres:16 started in
PT2.579483S
2023-09-20 14:50:59 INFO [main] postgres:16 - Container is started (JDBC URL:
jdbc:postgresql://localhost:32787/copsbootdb?loggerLevel=OFF)
2023-09-20 14:50:59 INFO [main] HikariPool - HikariPool-1 - Added connection
org.testcontainers.jdbc.ConnectionWrapper@1b8fa2fa
2023-09-20 14:50:59 INFO [main] HikariDataSource - HikariPool-1 - Start completed.
2023-09-20 14:50:59 INFO [main] BaseDatabaseType - Database:
jdbc:postgresql://localhost:32787/copsbootdb (PostgreSQL 16.0)
2023-09-20 14:50:59 WARN [main] Database - Flyway upgrade recommended: PostgreSQL 16.0 is
newer than this version of Flyway and support has not been tested. The latest supported
version of PostgreSQL is 15.
2023-09-20 14:50:59 INFO [main] JdbcTableSchemaHistory - Schema history table
"public"."flyway_schema_history" does not exist yet
2023-09-20 14:50:59 INFO [main] DbValidate - Successfully validated 1 migration
(execution time 00:00.015s)
2023-09-20 14:50:59 INFO [main] JdbcTableSchemaHistory - Creating Schema History table
"public"."flyway_schema_history" ...
2023-09-20 14:50:59 INFO [main] DbMigrate - Current version of schema "public": << Empty
Schema >>
2023-09-20 14:50:59 INFO [main] DbMigrate - Migrating schema "public" to version "1.0.0.1
- users"
2023-09-20 14:50:59 INFO [main] DbMigrate - Successfully applied 1 migration to schema
"public", now at version v1.0.0.1 (execution time 00:00.023s)

Since we removed H2, our integration test CopsbootApplicationTests is also broken since there is no
database anymore to connect to at startup. We can fix this similarly to what we did for the
UserRepositoryTest. Create an application-integration-test.properties with the same properties as
application-repository-test.properties and use @ActiveProfiles(SpringProfiles.INTEGRATION_TEST) to
activate those properties.

Summary
This chapter has shown how to use a relational database with Spring Boot and ensure that the
database is properly initialized by leveraging the Flyway integration in Spring Boot.

Further, you saw how to run an integration test against PostgreSQL with Testcontainers.

90
Validation

PART
EIGHT
Validation

91
Practical Guide to Building an API Back End with Spring Boot

An important part of a good API is having proper validation. Nothing is more frustrating than not
knowing why the API is not working because everything you try gives a generic 500 Internal Server
Error with no clear indication of the actual problem.

This chapter will show how to apply the standard validators, how to write your custom validator for a
single field or a group of fields in a class, and how to unit-test all that.

Built-in validators
We can use built-in validators, typically on the request object.

Let’s update CreateUserRequest with such a validation:

package com.example.copsboot.user.web;

import com.example.copsboot.user.AuthServerId;
import com.example.copsboot.user.CreateUserParameters;
import jakarta.validation.constraints.NotEmpty;
import org.springframework.security.oauth2.jwt.Jwt;

import java.util.UUID;

public record CreateUserRequest(@NotEmpty String mobileToken) { ①

public CreateUserParameters toParameters(Jwt jwt) {


AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject()));
String email = jwt.getClaimAsString("email");
return new CreateUserParameters(authServerId, email, mobileToken);
}
}

① Add the @NotEmpty annotation to validate that mobileToken should be a non-empty String.

We also need to update the controller method to indicate that we want validation to be checked by
adding @Valid:

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@PreAuthorize("hasRole('OFFICER')")
public UserDto createUser(@AuthenticationPrincipal Jwt jwt,
@Valid @RequestBody CreateUserRequest request) {
CreateUserParameters parameters = request.toParameters(jwt);
User user = userService.createUser(parameters);
return UserDto.fromUser(user);

92
Validation

Validation basically is as simple as adding the @Valid annotation in the controller method and using the
validation annotations on the request object.

The default validation annotations of the validation API can be found in the
jakarta.validation.constraints package:

• @AssertFalse,

• @AssertTrue,

• @DecimalMax,

• @Digits,

• @Email,

• @Future,

• @Max,

• @Min,

• @NotBlank,

• @NotEmpty,

• @NotNull,

• @Null,

• @Past,

• @Pattern, and

• @Size.

Because you have Hibernate Validator on the classpath, you also have access to the following in the
org.hibernate.validator.constraints package:

• @CreditCardNumber,

• Currency,

• @EAN,

• @Length,

• @LuhnCheck,

• @Mod10Check,

• @Mod11Check,

• @Range,

• @UniqueElements,

93
Practical Guide to Building an API Back End with Spring Boot

• @URL, and

• @UUID.

Unit test for a built-in validator


You can perform a unit test on this in the UserRestControllerTest:

@Test
void givenEmptyMobileToken_badRequestIsReturned() throws Exception {
mockMvc.perform(post("/api/users")
.with(jwt().jwt(builder -> builder.subject(UUID.randomUUID
().toString()))
.authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"mobileToken": ""
}
""")) ①
.andExpect(status().isBadRequest()) ②
.andDo(print()); ③

verify(userService, never()).createUser(any(CreateUserParameters.class)); ④
}

① Use an empty mobileToken to trigger the validation error.

② Check that you get back a 400 Bad Request.

③ Print the request and response so you can have a look.

④ Ensure the user service is never called. If there is a validation error, no user should be created.

Print the response using the static MockMvcResultHandlers.print() method. When running, it will output
something like this:

MockHttpServletRequest:
HTTP Method = POST
Request URI = /api/users
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"26"]
Body = {
"mobileToken": ""
}

94
Validation

Session Attrs = {}

Handler:
Type = com.example.copsboot.user.web.UserRestController
Method = com.example.copsboot.user.web.UserRestController#createUser(Jwt,
CreateUserRequest)

Async:
Async started = false
Async result = null

Resolved Exception:
Type = org.springframework.web.bind.MethodArgumentNotValidException

ModelAndView:
View name = null
View = null
Model = null

FlashMap:
Attributes = null

MockHttpServletResponse:
Status = 400
Error message = Invalid request content.
Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"0", Cache-
Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0",
X-Frame-Options:"DENY"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []

Note how the body is empty. You have protected your server against invalid input, and you notify the
client of this fact with the 400 Bad Request status code. However, it is impolite not to tell your
consumer what the problem is.

To do this, implement an exception handler.

Handling validation errors via an exception handler


With Spring Boot, you can react to the validation exception using an @ExceptionHandler. Since the
exception handler, in this case, can be reused for all controllers, you can define it in a separate class
annotated with @ControllerAdvice:

95
Practical Guide to Building an API Back End with Spring Boot

@ControllerAdvice ①
public class RestControllerExceptionHandler {

@ExceptionHandler ②
@ResponseBody ③
@ResponseStatus(HttpStatus.BAD_REQUEST) ④
public Map<String, List<FieldErrorResponse>> handle(MethodArgumentNotValidException
exception) { ⑤
return error(exception.getBindingResult()
.getFieldErrors()
.stream()
.map(fieldError -> new FieldErrorResponse(fieldError
.getField(), ⑥
fieldError
.getDefaultMessage()))
.collect(Collectors.toList()));
}

private Map<String, List<FieldErrorResponse>> error(List<FieldErrorResponse> errors)


{
return Collections.singletonMap("errors", errors);
}
}

① Mark this class as code that will apply to all controllers in the project.

② Mark this method as a method that should be called when an exception is thrown in a controller.

③ This indicates that the return value of the method should be directly used as the response body
(after being serialized to JSON).

④ Return this status code when this exception handler is triggered.

⑤ The method name can be anything, but the type of the argument determines the exception to which
this method will react. You want to react to MethodArgumentNotValidException here. Note that the
response output of the unit test above printed this exception class as Resolved Exception.

⑥ Using Java 8 stream operations, create a list of FieldErrorResponse objects so that the consumer of
your API will receive the name of the field that has a problem and an explanation of what is wrong.

Your exception handler uses the following simple data holder:

package com.example.copsboot.infrastructure.mvc;

public record FieldErrorResponse(String fieldName, String errorMesesage) {


}

96
Validation

Running the test again results in the following output:

MockHttpServletResponse:
Status = 400
Error message = null
Headers = [Content-Type:"application/json", X-Content-Type-Options:"nosniff",
X-XSS-Protection:"0", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate",
Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = application/json
Body = {"errors":[{"fieldName":"mobileToken","errorMesesage":"must not be
empty"}]}
Forwarded URL = null
Redirected URL = null
Cookies = []

You can now extend the unit test to check the fieldName property to ensure that your validation reports
the correct fault to your API consumer:

.andExpect(status().isBadRequest())
.andDo(print())
.andExpect(jsonPath("errors[0].fieldName").value("mobileToken")); ①

① Assert the response body to return the field name that has the validation error.

For a production application, you can use error-handling-spring-boot-starter to


automatically have nice JSON responses without having to implement an
@ExceptionHandler manually.

To try it out, add this dependency and remove the exception handler you just created:

 <dependency>
<groupId>io.github.wimdeblauwe</groupId>
<artifactId>error-handling-spring-boot-starter</artifactId>
<version>4.2.0</version>
</dependency>

Run the test again and have a look at the produced JSON response.

Custom field validator


It is also possible to write a custom validator. In this chapter, you will create a validator for single field.
The next section will show how to validate a complete object when considering multiple fields in the

97
Practical Guide to Building an API Back End with Spring Boot

validation.

To make things a bit more interesting, let’s extend your application to allow a police officer to post a
report to your API. Create the following entity for this:

@Entity
public class Report extends AbstractEntity<ReportId> {

private UserId reporterId;


private Instant dateTime;
private String description;

@ArtifactForFramework
protected Report() {
}

public Report(ReportId id, UserId reporterId, Instant dateTime, String description) {


super(id);
this.reporterId = reporterId;
this.dateTime = dateTime;
this.description = description;
}

public UserId getReporterId() {


return reporterId;
}

public Instant getDateTime() {


return dateTime;
}

public String getDescription() {


return description;
}
}

Now create the following classes/interfaces according to the same principles you used when creating
the User entity counterparts:

• ReportId,

• ReportRepository,

• ReportRepositoryCustom,

• ReportRepositoryImpl,

• CreateReportParameters, and

98
Validation

• ReportService.

Finally, in a web subpackage, create the ReportRestController:

@RestController
@RequestMapping("/api/reports")
public class ReportRestController {
private final ReportService service;
private final UserService userService;

public ReportRestController(ReportService service, UserService userService) {


this.service = service;
this.userService = userService;
}

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ReportDto createReport(@AuthenticationPrincipal Jwt jwt,
@Valid @RequestBody CreateReportRequest request) {
AuthServerId authServerId = new AuthServerId(UUID.fromString(jwt.getSubject()));
User user = userService.findUserByAuthServerId(authServerId)
.orElseThrow(() -> new UserNotFoundException(authServerId));
CreateReportParameters parameters = request.toParameters(user.getId());
Report report = service.createReport(parameters);
return ReportDto.fromReport(report, userService);
}
}

This has the following request object:

package com.example.copsboot.report.web;

import com.example.copsboot.report.CreateReportParameters;
import com.example.copsboot.user.UserId;

import java.time.Instant;

public record CreateReportRequest(Instant dateTime, String description) {


public CreateReportParameters toParameters(UserId userId) {
return new CreateReportParameters(userId, dateTime, description);
}
}

The POST returns a JSON response modeled by the ReportDto class:

99
Practical Guide to Building an API Back End with Spring Boot

public record ReportDto(ReportId id,


String reporter,
Instant dateTime,
String description) {

public static ReportDto fromReport(Report report, UserService userService) {


return new ReportDto(report.getId(),
userService.getUserById(report.getReporterId()).getEmail(),
report.getDateTime(),
report.getDescription());
}
}

This is the accompanying unit test that will show that the basic flow works:

@CopsbootControllerTest(ReportRestController.class)
public class ReportRestControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ReportService service;
@MockBean
private UserService userService;

@Test
public void officerIsAbleToPostAReport() throws Exception {

UserId userId = new UserId(UUID.randomUUID());


AuthServerId authServerId = new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-
98de-d8b4ae2750ac"));
User user = new User(userId,
"[email protected]",
authServerId,
"c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0");
when(userService.findUserByAuthServerId(authServerId))
.thenReturn(Optional.of(user));
when(userService.getUserById(userId))
.thenReturn(user);
when(service.createReport(any(CreateReportParameters.class)))
.thenReturn(new Report(new ReportId(UUID.randomUUID()),
userId,
Instant.parse("2023-04-11T22:59:03.189+02:00"),
"This is a test report description."));
mockMvc.perform(post("/api/reports")
.with(jwt().jwt(builder -> builder.subject(authServerId.value

100
Validation

().toString())
.claim("email", "[email protected]"))
.authorities(new SimpleGrantedAuthority("ROLE_OFFICER")))
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"dateTime": "2023-04-11T22:59:03.189+02:00",
"description": "This is a test report description."
}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("id").exists())
.andExpect(jsonPath("reporter").value("[email protected]"))
.andExpect(jsonPath("dateTime").value("2023-04-11T20:59:03.189Z"))
.andExpect(jsonPath("description").value("This is a test report
description."));
}
}

To show how to create a custom validator, let’s check whether or not the police officer described a
suspect in the submitted report. To do this, you will check if the word suspect appears in the report’s
description field.

The first thing you need to do is to create an annotation that you can then add to the description field.
For this example, call it @ValidReportDescription:

package com.example.copsboot.report.web;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD) ①
@Retention(RetentionPolicy.RUNTIME) ②
@Constraint(validatedBy = {ReportDescriptionValidator.class}) ③
public @interface ValidReportDescription {
String message() default "Invalid report description"; ④

Class<?>[] groups() default {}; ⑤

Class<? extends Payload>[] payload() default {}; ⑥


}

101
Practical Guide to Building an API Back End with Spring Boot

① Your annotation can be applied to a field of a class.

② The annotation must be available at run time.

③ The ReportDescriptionValidator class will contain your custom validation logic and will validate the
field.

④ This is the default message if validation fails.

⑤ Allows to specify validation groups for our constraints.

⑥ Can be used by clients of the Bean Validation API to assign custom payload objects to a constraint.

ReportDescriptionValidator validates the @ValidReportDescription annotation:

package com.example.copsboot.report.web;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class ReportDescriptionValidator implements ConstraintValidator


<ValidReportDescription, String> { ①

@Override
public void initialize(ValidReportDescription constraintAnnotation) { ②
}

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
boolean result = true;
if (!value.toLowerCase().contains("suspect")) { ③
result = false;
}
return result;
}
}

① A validator must implement the ConstraintValidator interface with two generic types. The first one
is the used annotation. The second is the type that the annotation can be attached to.

② The initialize method allows you to take information from the annotation to parameterize the
validation process.

③ Do the actual validation logic and return false if the validation fails.

The final step is using the annotation on your CreateReportRequest class:

package com.example.copsboot.report.web;

102
Validation

import com.example.copsboot.report.CreateReportParameters;
import com.example.copsboot.user.UserId;

import java.time.Instant;

public record CreateReportRequest(Instant dateTime, @ValidReportDescription String


description) {
public CreateReportParameters toParameters(UserId userId) {
return new CreateReportParameters(userId, dateTime, description);
}
}

To ensure that everything is working, write the following unit test:

Listing 31. ReportDescriptionValidatorTest.java

@Test
public void givenEmptyString_notValid() {
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { ①
Validator validator = factory.getValidator(); ②

CreateReportRequest parameters = new CreateReportRequest(Instant.now(), "");


Set<ConstraintViolation<CreateReportRequest>> violationSet = validator
.validate(parameters); ③
assertThat(violationSet).hasViolationOnPath("description"); ④
}
}

① Get the default ValidatorFactory.

② Ask for the Validator.

③ Using the Validator, validate the object.

④ Assert that there is a validation error on the description field.

You should of course also test the valid case:

Listing 32. ReportDescriptionValidatorTest.java

@Test
public void givenSuspectWordPresent_valid() {
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
Validator validator = factory.getValidator();

CreateReportRequest parameters = new CreateReportRequest(Instant.now(),


"The suspect was wearing a black hat.");

103
Practical Guide to Building an API Back End with Spring Boot

Set<ConstraintViolation<CreateReportRequest>> violationSet = validator


.validate(parameters);
assertThat(violationSet).hasNoViolations();
}
}

The assert statement uses a custom AssertJ assertion. See AssertJ custom assertion for
 ConstraintValidator tests for how that works.

Custom object validator


You can also validate a whole object instead of a single field. To do this, add two new properties to
report creation: trafficIncident and numberOfInvolvedCars. The logic will be that if trafficIncident is
true then numberOfInvolvedCars is supposed to be 1 or more.

First, adjust CreateReportRequest:

package com.example.copsboot.report.web;

import com.example.copsboot.report.CreateReportParameters;
import com.example.copsboot.user.UserId;

import java.time.Instant;

@ValidCreateReportRequest
public record CreateReportRequest(Instant dateTime,
@ValidReportDescription String description,
boolean trafficIncident,
int numberOfInvolvedCars) {
public CreateReportParameters toParameters(UserId userId) {
return new CreateReportParameters(userId, dateTime, description);
}
}

Note that your new annotation @ValidCreateReportRequest has been added as a class-level annotation.
This is the code you need for it:

package com.example.copsboot.report.web;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

104
Validation

import java.lang.annotation.Target;

//tag::class[]
@Target(ElementType.TYPE) ①
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {CreateReportRequestValidator.class}) ②
public @interface ValidCreateReportRequest {
String message() default "Invalid report";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};


}//end::class[]

① Indicate this annotation should be added to the class.

② The validator to use is CreateReportParametersValidator.

Finally, here is the validator itself:

public class CreateReportRequestValidator implements ConstraintValidator


<ValidCreateReportRequest, CreateReportRequest> { ①

@Override
public void initialize(ValidCreateReportRequest constraintAnnotation) {
}

@Override
public boolean isValid(CreateReportRequest value, ConstraintValidatorContext context)
{
boolean result = true;
if (value.trafficIncident() && value.numberOfInvolvedCars() <= 0) { ②
result = false;
}
return result;
}

① Use the correct generic types for the used annotation and class that is annotated.

② Do the correct test and return false when the combination of parameters is invalid.

Next up, write a test to validate your validation logic.

If trafficIncident is true and numberOfInvolvedCars is 0, then you should get a violation on the root path
(indicated with the empty string):

105
Practical Guide to Building an API Back End with Spring Boot

@Test
public void givenTrafficIndicentButInvolvedCarsZero_invalid() {
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
Validator validator = factory.getValidator();

CreateReportRequest parameters = new CreateReportRequest(Instant.now(),


"The suspect was wearing a black hat",
true,
0);
Set<ConstraintViolation<CreateReportRequest>> violationSet = validator.validate
(parameters);
assertThat(violationSet).hasViolationOnPath("");
}
}

If trafficIncident is true and numberOfInvolvedCars is 2, then there is no violation:

@Test
public void givenTrafficIndicent_involvedCarsMustBePositive() {
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
Validator validator = factory.getValidator();

CreateReportRequest parameters = new CreateReportRequest(Instant.now(),


"The suspect was wearing a black hat.",
true,
2);
Set<ConstraintViolation<CreateReportRequest>> violationSet = validator.validate
(parameters);
assertThat(violationSet).hasNoViolations();
}
}

If trafficIncident is false, then there is also no violation:

@Test
public void givenNoTrafficIndicent_involvedCarsDoesNotMatter() {
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
Validator validator = factory.getValidator();

CreateReportRequest parameters = new CreateReportRequest(Instant.now(),


"The suspect was wearing a black hat.",
false,
0);
Set<ConstraintViolation<CreateReportRequest>> violationSet = validator.validate

106
Validation

(parameters);
assertThat(violationSet).hasNoViolations();
}
}

Custom object validator using a Spring service


ReportDescriptionValidator and CreateReportParametersValidator are fairly simple as they do not need
any other class to perform the validation. But what if, when creating a new user, you must validate
that there is no existing user in the database with the new user’s mobile token?

Your validator will need a reference to the UserService to check this. Luckily, this is easy using Spring
Boot.

To get started, create a new annotation @ValidCreateUserRequest:

@Target(ElementType.TYPE) ①
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {CreateUserRequestValidator.class}) ②
public @interface ValidCreateUserRequest {
String message() default "Invalid user";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};


}

① This is a class-level annotation, so use ElementType.TYPE.

② Validation will be handled by CreateUserRequestValidator.

Next, create your actual validator:

public class CreateUserRequestValidator implements ConstraintValidator


<ValidCreateUserRequest, CreateUserRequest> {

private final UserService userService;

@Autowired
public CreateUserRequestValidator(UserService userService) { ①
this.userService = userService;
}

@Override

107
Practical Guide to Building an API Back End with Spring Boot

public void initialize(ValidCreateUserRequest constraintAnnotation) {

@Override
public boolean isValid(CreateUserRequest userRequest, ConstraintValidatorContext
context) {

boolean result = true;

if (userService.findUserByMobileToken(userRequest.mobileToken()).isPresent()) {

context.buildConstraintViolationWithTemplate(
"There is already a user with the given mobile token.")
.addPropertyNode("mobileToken").addConstraintViolation(); ③

result = false; ④
}

return result;
}
}

① Autowire the UserService to ask if there is already a user with the given mobile token.

② Check for the presence of a user.

③ If so, update the context object with the error.

④ Return false to indicate that the validation has failed.

And that is all there is to it. To prove it all works, create the following test:

@SpringBootTest ①
@ActiveProfiles(SpringProfiles.REPOSITORY_TEST)
public class CreateUserRequestValidatorTest {

@MockBean
private UserService userService; ②
@Autowired
private ValidatorFactory factory; ③

@Test
public void invalidIfAlreadyUserWithGivenMobileToken() {

String mobileToken = "abc123";


when(userService.findUserByMobileToken(mobileToken))

108
Validation

.thenReturn(Optional.of(new User(new UserId(UUID.randomUUID()),


"[email protected]",
new AuthServerId(UUID.randomUUID()),
mobileToken)));

Validator validator = factory.getValidator(); ④

CreateUserRequest request = new CreateUserRequest(mobileToken);


Set<ConstraintViolation<CreateUserRequest>> violationSet = validator.validate
(request); ⑤
assertThat(violationSet).hasViolationSize(2)
.hasViolationOnPath("mobileToken"); ⑥
}

@Test
public void validIfNoUserWithGivenMobileToken() {
String mobileToken = "abc123";
when(userService.findUserByMobileToken(mobileToken))
.thenReturn(Optional.empty());

Validator validator = factory.getValidator();

CreateUserRequest request = new CreateUserRequest(mobileToken);


Set<ConstraintViolation<CreateUserRequest>> violationSet = validator.validate
(request);
assertThat(violationSet).hasNoViolations();
}
}

① Start the full application context in the test. We need to do this to test that the injection of the Spring
component into the validator works.

② Create a mock UserService so you can tell it how to behave.

③ Autowire the ValidatorFactory so you can do your validation test.

④ Get the actual Validator from the factory.

⑤ Validate the CreateUserRequest object.

⑥ Check if there is indeed a validator error on the mobileToken path.

There are two validation errors in violationSet because the object itself is invalid
 (since your annotation applies to the object), and you have manually added a
violation on the email field in your validator.

Summary

109
Practical Guide to Building an API Back End with Spring Boot

You have seen how to use validators and implement your custom validation. You have written unit
tests to ensure the validators are correct.

In the next chapter, you will learn about uploading files.

110
File Upload

PART
NINE
File Upload

111
Practical Guide to Building an API Back End with Spring Boot

Many APIs need to support file uploads. In many cases, these are images but could be any kind of file.
This chapter will show how to support this and give some pointers on validating the incoming file.

Upload a file
As an example, you will allow the user of your API to add an image when creating a new report. The
first thing you must do is to add an extra field to CreateReportParameters:

package com.example.copsboot.report.web;

import com.example.copsboot.report.CreateReportParameters;
import com.example.copsboot.user.UserId;
import jakarta.validation.constraints.NotNull;
import org.springframework.web.multipart.MultipartFile;

import java.time.Instant;

@ValidCreateReportRequest
public record CreateReportRequest(
Instant dateTime,
@ValidReportDescription String description,
boolean trafficIncident,
int numberOfInvolvedCars,
@NotNull MultipartFile image ①
) {
public CreateReportParameters toParameters(UserId userId) {
return new CreateReportParameters(userId, dateTime, description);
}
}

① This is the image field of type org.springframework.web.multipart.MultipartFile.

Next, update the method signature of your controller. This is what you had:

Listing 33. com.example.copsboot.report.web.ReportRestController

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ReportDto createReport(@AuthenticationPrincipal Jwt jwt,
@Valid @RequestBody CreateReportRequest request) {

Because you now want to allow files to be uploaded, you can no longer use a JSON body. By removing
@RequestBody, the method will permit the use of multipart/form-data instead:

112
File Upload

Listing 34. com.example.copsboot.report.web.ReportRestController

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ReportDto createReport(@AuthenticationPrincipal Jwt jwt,
@Valid CreateReportRequest request) {

To ensure that this all works properly, update ReportRestControllerTest to validate it:

@CopsbootControllerTest(ReportRestController.class)
public class ReportRestControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ReportService service;
@MockBean
private UserService userService;

@Test
public void officerIsAbleToPostAReport() throws Exception {

UserId userId = new UserId(UUID.randomUUID());


AuthServerId authServerId = new AuthServerId(UUID.fromString("eaa8b8a5-a264-48be-
98de-d8b4ae2750ac"));
User user = new User(userId,
"[email protected]",
authServerId,
"c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0");
when(userService.findUserByAuthServerId(authServerId))
.thenReturn(Optional.of(user));
when(userService.getUserById(userId))
.thenReturn(user);
when(service.createReport(any(CreateReportParameters.class)))
.thenReturn(new Report(new ReportId(UUID.randomUUID()),
userId,
Instant.parse("2023-04-11T22:59:03.189+02:00"),
"This is a test report description. The suspect was wearing a
black hat."));
mockMvc.perform(multipart("/api/reports") ①
.file(new MockMultipartFile("image", "picture.png", MediaType
.IMAGE_PNG_VALUE, new byte[]{1,2,3})) ②
.param("dateTime", "2023-04-11T22:59:03.189+02:00") ③
.param("description", "This is a test report description. The
suspect was wearing a black hat.")
.param("trafficIncident", "false")

113
Practical Guide to Building an API Back End with Spring Boot

.param("numberOfInvolvedCars", "0")
.with(jwt().jwt(builder -> builder.subject(authServerId.value
().toString())
.claim("email", "[email protected]"))
.authorities(new SimpleGrantedAuthority(
"ROLE_OFFICER"))))
.andExpect(status().isCreated())
.andExpect(jsonPath("id").exists())
.andExpect(jsonPath("reporter").value("[email protected]"))
.andExpect(jsonPath("dateTime").value("2023-04-11T20:59:03.189Z"))
.andExpect(jsonPath("description").value("This is a test report
description. The suspect was wearing a black hat."));
}
}

① Use MockMvcRequestBuilders.multipart() to simulate uploading a file.

② Specify the MultipartFile via the file() method. Use MockMultipartFile from Spring Test for your
testing. Note that it is important that the first argument ("image") of the constructor matches the
field name in CreateReportRequest.

③ Other fields from the CreateReportRequest object are added via the param() method.

File-size validation
One common task is ensuring that the file size does not exceed a certain limit to protect your server.
Spring Boot lets you easily configure this. Just add the following lines to application.properties:

spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB

• spring.servlet.multipart.max-file-size specifies the maximum size per file in megabytes (MB),


kilobytes (KB), or bytes (no suffix).

• spring.servlet.multipart.max-request-size specifies the maximum size for the whole multipart


request.

With these properties in place, when the file is too big, the application will give a
org.springframework.web.multipart.MaxUploadSizeExceededException. By default, this would return a 500
Internal Server Error response, but the proper error response code is 413 Payload Too Large. In order
to return a proper error status code and message in our API, we should extend our
RestControllerExceptionHandler with an additonal handler method:

114
File Upload

Listing 35. com.example.copsboot.infrastructure.mvc.RestControllerExceptionHandler

@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<Map<String, String>> maxUploadSizeExceeded
(MaxUploadSizeExceededException e) {
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
.body(Map.of("code", "MAX_UPLOAD_SIZE_EXCEEDED",
"description", e.getMessage()));
}

This will return a response like this when the exception is triggered:

HTTP/1.1 413
Content-Type: application/json

{
"code": "MAX_UPLOAD_SIZE_EXCEEDED",
"description": "Maximum upload size exceeded"
}

Writing a test for this behavior is not so easy. You can not use MockMvc to test this like we did before. As
Tomcat, not Spring Boot itself, handles this, you need to set up an integration test with an embedded
Tomcat for testing. We also need to start a Keycloak server since we are now dealing with the actual
security layer of our application. Luckily, we can use Testcontainers again for this.

Add the testcontainers-keycloak dependency:

<dependency>
<groupId>com.github.dasniko</groupId>
<artifactId>testcontainers-keycloak</artifactId>
<version>3.0.0</version>
<scope>test</scope>
</dependency>

Since we will no longer use MockMvc, we will use the REST Assured library to test HTTP requests and
validate the responses.

Add that library as well to your Maven pom.xml:

<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>

115
Practical Guide to Building an API Back End with Spring Boot

</dependency>

There is no need to set a version as Spring Boot has version management for REST Assured.

You can now create your integration test. This is the full test, but I will explain it bit by bit below.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) ①
@ActiveProfiles(SpringProfiles.INTEGRATION_TEST)
public class ReportRestControllerIntegrationTest {

private static final String REALM_NAME = "copsboot";


private static final String ROLE_NAME = "OFFICER";
private static final String INTEGRATION_TEST_CLIENT_ID = "integration-test-client";
private static final String TEST_USER_NAME = "[email protected]";
private static final String TEST_USER_PASSWORD = "test1234";

@LocalServerPort
private int serverport; ②

static KeycloakContainer keycloak = new KeycloakContainer


("quay.io/keycloak/keycloak:22.0.1"); ③
private static String clientSecret;

@BeforeAll
static void beforeAll() {
keycloak.start(); ④
Keycloak client = keycloak.getKeycloakAdminClient(); ⑤

KeycloakAdminClientFacade clientFacade = new KeycloakAdminClientFacade(client);



clientFacade.createRealm(REALM_NAME);
clientFacade.createRealmRole(REALM_NAME, ROLE_NAME);
clientFacade.createUser(REALM_NAME, TEST_USER_NAME, TEST_USER_PASSWORD,
ROLE_NAME);
clientSecret = clientFacade.createClient(REALM_NAME, INTEGRATION_TEST_CLIENT_ID);
}

@AfterAll
static void afterAll() {
keycloak.stop(); ⑦
}

@AfterEach
void afterEach(@Autowired UserRepository userRepository) {
userRepository.deleteAll();
}

116
File Upload

@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("com.c4-soft.springaddons.oidc.ops[0].iss", () -> keycloak
.getAuthServerUrl() + "/realms/" + REALM_NAME); ⑧
registry.add("com.c4-soft.springaddons.oidc.ops[0].authorities[0].path", () ->
"$.realm_access.roles"); ⑨
registry.add("com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix", () ->
"ROLE_");
}

@BeforeEach
public void setup() {
RestAssured.port = serverport; ⑩
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); ⑪
}

@Test
public void officerIsUnableToPostAReportIfFileSizeIsTooBig() {
String token = getToken(); ⑫

given()
.header("Authorization", "Bearer " + token) ⑬
.contentType(ContentType.JSON)
.body("""
{
"mobileToken":
"c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"
}
""")
.post("/api/users") ⑭
.then()
.statusCode(HttpStatus.CREATED.value()); ⑮

given()
.header("Authorization", "Bearer " + token)
.multiPart(new MultiPartSpecBuilder(new byte[2_000_000]) ⑯
.fileName("picture.png")
.controlName("image")
.mimeType("image/png")
.build())
.formParam("dateTime", "2018-04-11T22:59:03.189+02:00")
.formParam("description", "The suspect is wearing a black hat.")
.formParam("trafficIncident", "false")
.formParam("numberOfInvolvedCars", "0")
.when()
.post("/api/reports")

117
Practical Guide to Building an API Back End with Spring Boot

.then()
.statusCode(HttpStatus.PAYLOAD_TOO_LARGE.value()); ⑰
}

private String getToken() {


RestTemplate restTemplate = new RestTemplate(); ⑱
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

MultiValueMap<String, String> map = new LinkedMultiValueMap<>();


map.put("grant_type", Collections.singletonList("password")); ⑲
map.put("client_id", Collections.singletonList(INTEGRATION_TEST_CLIENT_ID));
map.put("client_secret", Collections.singletonList(clientSecret));
map.put("username", Collections.singletonList(TEST_USER_NAME));
map.put("password", Collections.singletonList(TEST_USER_PASSWORD));
KeycloakToken token =
restTemplate.postForObject(
keycloak.getAuthServerUrl() + "/realms/" + REALM_NAME +
"/protocol/openid-connect/token", ⑳
new HttpEntity<>(map, httpHeaders),
KeycloakToken.class);

assert token != null;


return token.accessToken();
}

private record KeycloakToken(@JsonProperty("access_token") String accessToken) {


}
}

The test starts Keycloak via Testcontainers by creating a KeycloakContainer instance and calling start
on it:

static KeycloakContainer keycloak = new KeycloakContainer


("quay.io/keycloak/keycloak:22.0.1");

@BeforeAll
static void beforeAll() {
keycloak.start();
...
}

@AfterAll
static void afterAll() {
keycloak.stop();

118
File Upload

Inside the beforeAll method, we set up our Keycloak with a realm, a role, a user, and a client:

@BeforeAll
static void beforeAll() {
keycloak.start();
Keycloak client = keycloak.getKeycloakAdminClient(); ①

KeycloakAdminClientFacade clientFacade = new KeycloakAdminClientFacade(client);



clientFacade.createRealm(REALM_NAME);
clientFacade.createRealmRole(REALM_NAME, ROLE_NAME);
clientFacade.createUser(REALM_NAME, TEST_USER_NAME, TEST_USER_PASSWORD,
ROLE_NAME);
clientSecret = clientFacade.createClient(REALM_NAME, INTEGRATION_TEST_CLIENT_ID);

}

① From the Keycloak container, we can retrieve the admin client, which allows us to administer
Keycloak like we have done manually through the web interface.

② Use a little helper class that we created to configure our test realm with a test user and a test client.

③ Keycloak generates a client secret for our OAuth client; we store that in a field to be able to use it
later to get a token from Keycloak for our user.

This KeycloakAdminClientFacade looks like this:

package com.example.copsboot.report.web;

import jakarta.ws.rs.core.Response;
import org.keycloak.admin.client.CreatedResponseUtil;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.*;
import org.keycloak.representations.idm.*;

import java.util.Collections;
import java.util.List;

public class KeycloakAdminClientFacade {


private final Keycloak keycloak;

public KeycloakAdminClientFacade(Keycloak keycloak) {


this.keycloak = keycloak;

119
Practical Guide to Building an API Back End with Spring Boot

public void createRealm(String realmName) {


RealmRepresentation realmRepresentation = new RealmRepresentation();
realmRepresentation.setRealm(realmName);
realmRepresentation.setEnabled(true);
RealmsResource realmsResource = keycloak.realms();
realmsResource.create(realmRepresentation);
}

public void createRealmRole(String realmName, String roleName) {


RealmResource copsbootRealm = keycloak.realm(realmName);
RoleRepresentation roleRepresentation = new RoleRepresentation();
roleRepresentation.setName(roleName);
copsbootRealm.roles().create(roleRepresentation);
}

public void createUser(String realmName, String username, String password, String


roleName) {
UserRepresentation userRepresentation = new UserRepresentation();
userRepresentation.setUsername(username);
userRepresentation.setEnabled(true);
CredentialRepresentation credentialRepresentation = new
CredentialRepresentation();
credentialRepresentation.setTemporary(false);
credentialRepresentation.setType(CredentialRepresentation.PASSWORD);
credentialRepresentation.setValue(password);
userRepresentation.setCredentials(List.of(credentialRepresentation));
RealmResource realmResource = keycloak.realm(realmName);
UsersResource usersResource = realmResource.users();
Response response = usersResource.create(userRepresentation);
String userId = CreatedResponseUtil.getCreatedId(response);

UserResource userResource = usersResource.get(userId);

userResource.resetPassword(credentialRepresentation);
RoleRepresentation roleRepresentation = realmResource.roles().get(roleName
).toRepresentation();
userResource.roles().realmLevel().add(Collections.singletonList
(roleRepresentation));
}

public String createClient(String realmName, String clientId1) {


RealmResource realmResource = keycloak.realm(realmName);
ClientRepresentation clientRepresentation = new ClientRepresentation();
clientRepresentation.setClientId(clientId1);
clientRepresentation.setDirectAccessGrantsEnabled(true);

120
File Upload

Response response = realmResource.clients().create(clientRepresentation);


String clientId = CreatedResponseUtil.getCreatedId(response);
ClientResource clientResource = realmResource.clients().get(clientId);
CredentialRepresentation secret = clientResource.getSecret();
return secret.getValue();
}
}

Since our Keycloak runs in a container, the port it runs on is random, as seen from our computer. This
means we need a way to update the com.c4-soft.springaddons.oidc.ops[0].iss property from
http://localhost:8180/realms/copsboot to the URL with the random port. Spring Boot allows us to do
this via @DynamicPropertySource like this:

@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("com.c4-soft.springaddons.oidc.ops[0].iss", () -> keycloak
.getAuthServerUrl() + "/realms/" + REALM_NAME); ①
registry.add("com.c4-soft.springaddons.oidc.ops[0].authorities[0].path", () ->
"$.realm_access.roles"); ②
registry.add("com.c4-soft.springaddons.oidc.ops[0].authorities[0].prefix", () ->
"ROLE_");
}

① Pass in a lambda that will be called after Testcontainers is ready to set the issuer URL based on the
information we get from the Keycloak container.

② We need to re-apply the path and prefix values because of how merging complex types works.

In the test itself, we need to get an access token from Keycloak for our user. For this purpose, the
getToken() helper method was created:

private String getToken() {


RestTemplate restTemplate = new RestTemplate(); ①
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

MultiValueMap<String, String> map = new LinkedMultiValueMap<>();


map.put("grant_type", Collections.singletonList("password")); ②
map.put("client_id", Collections.singletonList(INTEGRATION_TEST_CLIENT_ID));
map.put("client_secret", Collections.singletonList(clientSecret));
map.put("username", Collections.singletonList(TEST_USER_NAME));
map.put("password", Collections.singletonList(TEST_USER_PASSWORD));
KeycloakToken token =
restTemplate.postForObject(
keycloak.getAuthServerUrl() + "/realms/" + REALM_NAME +

121
Practical Guide to Building an API Back End with Spring Boot

"/protocol/openid-connect/token", ③
new HttpEntity<>(map, httpHeaders),
KeycloakToken.class);

assert token != null;


return token.accessToken(); ④
}

private record KeycloakToken(@JsonProperty("access_token") String accessToken) { ⑤


}

① Use RestTemplate to do a HTTP call to Keycloak.

② Use the password grant.

We use the password grant here while it is deprecated. This is not a big issue as we
only do this for this test. It should not be used for our actual mobile application. It
is easier to get a token this way than to simulate opening a browser and getting it
back via the normal Authorization Code flow (like you do when you get a token
 through Postman).

Another alternative is to use client_credentials grant as you can view your tests as
a "client".

③ Return the access token.

④ Record that represents the JSON response structure of the response that Keycloak gives us.

With all this in place, we can view the actual test:

@Test
public void officerIsUnableToPostAReportIfFileSizeIsTooBig() {
String token = getToken(); ①

given()
.header("Authorization", "Bearer " + token) ②
.contentType(ContentType.JSON)
.body("""
{
"mobileToken":
"c41536a5a8b9d3f14a7e5472a5322b5e1f76a6e7a9255c2c2e7e0d3a2c5b9d0"
}
""")
.post("/api/users") ③
.then()
.statusCode(HttpStatus.CREATED.value()); ④

122
File Upload

given()
.header("Authorization", "Bearer " + token)
.multiPart(new MultiPartSpecBuilder(new byte[2_000_000]) ⑤
.fileName("picture.png")
.controlName("image")
.mimeType("image/png")
.build())
.formParam("dateTime", "2018-04-11T22:59:03.189+02:00")
.formParam("description", "The suspect is wearing a black hat.")
.formParam("trafficIncident", "false")
.formParam("numberOfInvolvedCars", "0")
.when()
.post("/api/reports")
.then()
.statusCode(HttpStatus.PAYLOAD_TOO_LARGE.value()); ⑥
}

① Get an authentication token from Keycloak for doing the call to our Spring Boot application.

② Use the token in an Authorization header.

③ Call the POST /api/users endpoint so our Spring Boot application knows about the user that was
created in Keycloak.

④ Check that the linking of the Keycloak user as an application user was succesful.

⑤ Specify the file size via the byte array to be larger than that allowed in your
spring.http.multipart.max-file-size setting.

⑥ Validate that you get a 413 Payload Too Large back.

Running the test will trigger the following flow:

1. Testcontainers starts a Docker container with Keycloak.

2. Our test configures Keycloak through the admin client.

3. The Spring Boot application is started.

4. A token is requested to Keycloak.

5. A request is made to our application to link the Keycloak user to an application user (via POST
/api/users).

6. A POST to /api/reports is done with a payload that exceeds the allowed size.

7. The test validatese that a proper error is returned from our application.

Quite some work, but also very nice that we are using all the real components in this test so we are
sure everything works together. And we only need to write it once so it will serve us for the rest of the
project to ensure this functionality will never break.

123
Practical Guide to Building an API Back End with Spring Boot

Summary
This chapter has shown how to implement a file upload for your API and how to set a maximum file
size. We also wrote an integration test with Testcontainers to ensure the maximum file size is honored.

124
Action!

Action!
I hope you have learned a lot from this book and I am looking forward to seeing more great
applications using the amazing Spring Boot framework.

Additional reading
If you want to learn more, here are some suggestions:

• Spring Boot Reference Documentation — Phillip Webb, Dave Syer, Josh Long, Stéphane Nicoll, Rob
Winch, Andy Wilkinson, Marcel Overdijk, Christian Dupuis, Sébastien Deleuze Michael Simons,
Vedran Pavić, Jay Bryant, Madhura Bhave, Eddú Meléndez, Scott Frederick, Moritz Halbritter

• Learning Spring Boot 3.0 — Greg L. Turnquist (Packt Publishing, 2022)

• Spring in Action (Sixth edition) — Craig Walls (Manning, 2022)

• Spring Tips — YouTube playlist

• Cloud Native Spring in Action — Thomas Vitale (Manning, 2022)

125
Practical Guide to Building an API Back End with Spring Boot

About the Author


Wim Deblauwe is a software engineer who has been working mainly with Java for the past 25 years.
He has developed and designed various software projects that have seen deployments worldwide. He
also loves to guide and teach others about topics like Java, Spring, and Thymeleaf.

He is conference speaker and the author of Taming Thymeleaf and Modern frontends with htmx which
are two books that focus on full-stack Java development using Spring Boot, Thymeleaf and htmx.

He also regularly blogs about these topics on his personal website: https://www.wimdeblauwe.com/

126

You might also like