Build a simple app with Spring Boot

Build a Simple App With Spring Boot

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.

Lorenzo Miscoli

Software Developer specialized in creating and designing web applications. I have always loved technology and dreamed of working in the IT world, to make full use of my creativity and realize my ideas.
Scroll to Top