This tutorial will help you build a simple app with Spring Boot and see what makes it such a great framework. We’ll look at some basic configuration, the front end, data manipulation, and finally error handling.
Dependency Configuration
First of all, use Spring Initializr to generate the project. Initializr offers a fast way to push in all the dependencies needed for an application and does much of the configuration for you.
Project is based on the parent.
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.0</version> </parent>
Other dependencies are pretty simple.
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency> </dependencies>
Application Configuration
Once the dependencies are configured, create a simple class with the main method.
@SpringBootApplication public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } }
The @SpringBootApplication annotation marks the class it’s in as root, so behind the scenes it’s the same as using @Configuration, @EnableAutoConfiguration, and @ComponentScan together.
After that create an application.properties file which for now has only one property.
server.port=8085
server.port changes the port that the server uses, the default is 8080. Obviously there are many other properties of Sping Boot available.
Front End
Now add a simple front-end by using Thymeleaf
First add spring-boot-starter-thymeleaf to your dependencies in the pom.xml.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
This dependency enables Thymeleaf, and no other additional configuration is required.
Now you can add the following properties in the application.properties file.
spring.thymeleaf.cache=false spring.thymeleaf.enabled=true spring.thymeleaf.prefix=classpath:/views/ spring.thymeleaf.suffix=.html spring.application.name=Spring Boot Starter
Define a basic controller and home page with a welcome message.
@Controller public class MainController { private String appName; public MainController(@Value("${spring.application.name}") final String appName) { this.appName = appName; } @GetMapping("/") public String homePage(Model model) { model.addAttribute("appName", appName); return "home"; } }
After that, create the home.html page
<html> <head> <title>Home Page</title> </head> <body> <h1>Hello !</h1> <p> Welcome to <span th:text="${appName}">Our App</span> </p> </body> </html>
Note how we defined the spring.application.name property in the application.properties file and then injected it into the controller and finally displayed on the home page.
Security
Now add the spring-boot-starter-security dependency in your pom.xml.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
Once this dependency is in the application classpath, all endpoints will be secure by default, using the httpBasic or formLogin based on the Spring Security content negotiation strategy.
That’s why you usually create your own security configuration class, extending the WebSecurityConfigurerAdapter class.
@Configuration public class SecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.csrf(csrf -> csrf.disable()).authorizeHttpRequests((auth) -> auth.anyRequest().permitAll()).build(); } }
In this example, we grant access to the endpoints to anyone.
Persistence
Start by creating the User entity.
@Entity @Table(name = "[user]") public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; @Column(nullable = false, unique = true) private String username; @Column(nullable = false) private String mail; // getters and setters }
and its repository, making great use of Spring Data.
public interface UserRepository extends CrudRepository<User, Long> { public Optional<User> findByUsername(String username); }
Finally, of course, you need to configure persistence.
To keep things simple, we use an in-memory database H2, so you don’t have any external dependencies when running your project.
Once the H2 dependency is included, Spring Boot automatically detects it and sets persistence without any further configuration beyond data source properties.
spring.application.name=SpringBootStarter spring.datasource.driver-class-name=org.h2.Driver spring.datasource.url=jdbc:h2:mem:bootapp;DB_CLOSE_DELAY=-1 spring.datasource.username=test spring.datasource.password=
Service
It’s time to create the UserService class. CRUD operations will be performed in this class, calling the repository directly.
@Service public class UserService { private final UserRepository userRepository; public UserService(final UserRepository userRepository) { this.userRepository = userRepository; } public List<User> findAll() { return (List<User>) userRepository.findAll(); } public User findById(Long id) { return userRepository.findById(id).orElseThrow(UserNotFoundException::new); } public User findByUsername(String username) { return userRepository.findByUsername(username).orElseThrow(UserNotFoundException::new); } public User insert(User user) { return userRepository.save(user); } public User update(User user, Long id) { if (user.getId() != id) { throw new UserMismatchException(); } findById(id); return userRepository.save(user); } public void delete(Long id) { findById(id); userRepository.deleteById(id); } }
Controller
Now create the UserController class, responsible for exposing the User resource.
@RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; public UserController(final UserService userService) { this.userService = userService; } @GetMapping public ResponseEntity<List<User>> findAll() { return ResponseEntity.ok(userService.findAll()); } @GetMapping("/username/{username}") public ResponseEntity<User> findByUsername(@PathVariable String username) { return ResponseEntity.ok(userService.findByUsername(username)); } @GetMapping("/{id}") public ResponseEntity<User> findById(@PathVariable Long id) { return ResponseEntity.ok(userService.findById(id)); } @PostMapping public ResponseEntity<User> insert(@RequestBody User user) { User newUser = userService.insert(user); URI loc = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(newUser.getId()) .toUri(); return ResponseEntity.created(loc).body(newUser); } @PutMapping("/{id}") public ResponseEntity<User> update(@RequestBody User user, @PathVariable Long id) { return ResponseEntity.ok(userService.update(user, id)); } @DeleteMapping("/{id}") public ResponseEntity<Object> delete(@PathVariable Long id) { userService.delete(id); return ResponseEntity.noContent().build(); } }
Given the nature of this API, we use the @RestController annotation here, which is equivalent to a @Controller together with @ResponseBody, so that each method marshals the returned resource directly on the HTTP response.
Error Handling
Now that the main application is ready, let’s focus on a simple centralized error handling mechanism using the @ControllerAdvice annotation.
@ControllerAdvice public class ExceptionController extends ResponseEntityExceptionHandler { @ExceptionHandler({ UserNotFoundException.class }) protected ResponseEntity<Object> handleNotFound(Exception ex, WebRequest request) { return handleExceptionInternal(ex, "User not found", new HttpHeaders(), HttpStatus.NOT_FOUND, request); } @ExceptionHandler({ UserMismatchException.class }) public ResponseEntity<Object> handleBadRequest(Exception ex, WebRequest request) { return handleExceptionInternal(ex, "Wrong user Id", new HttpHeaders(), HttpStatus.BAD_REQUEST, request); } }
After that, create the custom UserNotFoundException.
public class UserNotFoundException extends RuntimeException { public UserNotFoundException(final String message, final Throwable cause) { super(message, cause); } // other constructors }
and finally UserMismatchException.
public class UserMismatchException extends RuntimeException { public UserMismatchException(String message, Throwable cause) { super(message, cause); } // other constructors }
This should give you an idea of what is possible to handle with the global exception handling mechanism.
Additionally Spring Boot also provides an /error mapping by default. We can customize its display by creating a simple error.html.
<html lang="en"> <head><title>Error Occurred</title></head> <body> <h1>Error Occurred!</h1> <b>[<span th:text="${status}">status</span>] <span th:text="${error}">error</span> </b> <p th:text="${message}">message</p> </body> </html>
As always Spring Boot allows us to manage the property.
server.error.path=/error
Conclusion
In this tutorial we have seen how to build a simple app with Spring Boot and some tools the framework makes available to us. Obviously there is a lot more to say about this framework but as a start it is perfectly fine.
Source code is on GitHub.