You have an idea for a Java web application. You've heard about Spring Boot 3, but most tutorials are either outdated (Java 8, Spring Boot 1.x) or buried in excessive configuration. The real problem: wasting time on scaffolding instead of building what matters. At Meteora Web, we see it daily: developers spending hours choosing the right starter, figuring out why the application context won't start, or fighting incompatible versions. This guide takes you from zero to a full Spring Boot 3 web application – working and ready to extend. No fluff, just copy-paste-and-edit code.
Why Spring Boot 3 Over Version 2
Spring Boot 3 is a generational leap. Built on Spring Framework 6, it requires Java 17 or higher. That means records, pattern matching, sealed classes, text blocks – features that make code cleaner and safer. Under the hood, it uses Jakarta EE 9+ (no more javax.servlet), natively supports GraalVM for native images, and has a much cleaner dependency management. If you start from scratch with Spring Boot 3, you never need to backport. And if you're migrating from 2.x, the path is straightforward – but we're building from zero, so full speed ahead.
Project Setup: 60 Seconds with Spring Initializr
Forget downloading ZIPs and importing manually. We use Spring Initializr: choose Maven (or Gradle), Java 17+, Spring Boot 3.x. The minimal dependencies for a complete web application:
- Spring Web – for REST and MVC
- Spring Data JPA – database access
- H2 Database – for development (swap later with PostgreSQL or MySQL)
- Spring Boot DevTools – hot reload, don't skip it
- Lombok – reduces boilerplate
- Thymeleaf – HTML template engine (if you need integrated frontend)
- Validation – jakarta.validation for input validation
Generate the project, unzip it, and open it in your favorite IDE (IntelliJ or VS Code with Spring Boot extension).
Package Structure
Don't dump everything in the root package. Organize by feature: controller, service, repository, model, dto. Example:
com.meteoraweb.demo
├── controller
├── service
├── repository
├── model
└── dto
First REST Controller: A Real Hello World
Let's create a controller that returns JSON. We use a record for the DTO – Java 17 allows it and eliminates boilerplate.
package com.meteoraweb.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1")
public class GreetingController {
@GetMapping("/greeting")
public GreetingResponse greet() {
return new GreetingResponse("Hello from Meteora Web");
}
public record GreetingResponse(String message) {}
}
Run the application with mvn spring-boot:run and hit http://localhost:8080/api/v1/greeting. See JSON? It works. You just built an endpoint in 10 lines.
Adding Business Logic and Persistence
Now let's create a JPA entity to store messages. A simple model: Greeting with id, text, and creation date.
package com.meteoraweb.demo.model;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "greetings")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Greeting {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String text;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@PrePersist
public void prePersist() {
this.createdAt = LocalDateTime.now();
}
}
Repository:
package com.meteoraweb.demo.repository;
import com.meteoraweb.demo.model.Greeting;
import org.springframework.data.jpa.repository.JpaRepository;
public interface GreetingRepository extends JpaRepository<Greeting, Long> {
}
Service:
package com.meteoraweb.demo.service;
import com.meteoraweb.demo.model.Greeting;
import com.meteoraweb.demo.repository.GreetingRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class GreetingService {
private final GreetingRepository repository;
public Greeting save(String text) {
Greeting greeting = Greeting.builder().text(text).build();
return repository.save(greeting);
}
public List<Greeting> findAll() {
return repository.findAll();
}
}
Update the controller to use the service:
@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class GreetingController {
private final GreetingService service;
@PostMapping("/greeting")
public Greeting create(@RequestBody GreetingRequest request) {
return service.save(request.text());
}
@GetMapping("/greeting")
public List<Greeting> getAll() {
return service.findAll();
}
public record GreetingRequest(String text) {}
}
Now you have a basic CRUD API with an in-memory H2 database. Test with curl or Postman.
Frontend with Thymeleaf: A Simple Page
If you need an integrated web interface, Thymeleaf is the quickest choice. Create an MVC controller:
package com.meteoraweb.demo.controller;
import com.meteoraweb.demo.service.GreetingService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
@RequiredArgsConstructor
public class HomeController {
private final GreetingService service;
@GetMapping("/")
public String home(Model model) {
model.addAttribute("greetings", service.findAll());
return "home";
}
}
Template src/main/resources/templates/home.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Meteora Web - Greetings</title>
</head>
<body>
<h1>Messages</h1>
<ul>
<li th:each="g : ${greetings}" th:text="${g.text} + ' (' + ${g.createdAt} + ')'"></li>
</ul>
<form method="post" action="/api/v1/greeting">
<input type="text" name="text" placeholder="Write a message" />
<button type="submit">Send</button>
</form>
</body>
</html>
Note: the form POST should be handled by the REST controller. For simplicity, you can either add a POST method in the MVC controller or use JavaScript. We'll keep it as is – you can extend it.
Validation and Error Handling
Never trust user input. Add validation on the DTO:
public record GreetingRequest(@NotBlank String text) {}
And enable @Valid in the controller:
@PostMapping("/greeting")
public Greeting create(@Valid @RequestBody GreetingRequest request) { ... }
To handle errors uniformly, create a @ControllerAdvice:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(e -> errors.put(e.getField(), e.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<String> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.notFound().build();
}
}
Testing: Don't Wait for Production
Spring Boot 3 makes testing easy with @SpringBootTest and @WebMvcTest.
@WebMvcTest(GreetingController.class)
class GreetingControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldReturnGreetings() throws Exception {
mockMvc.perform(get("/api/v1/greeting"))
.andExpect(status().isOk());
}
}
For database integration, use @DataJpaTest with @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY). Write tests for every repository and service. If you don't test, you don't know it works.
Production Configuration
Before going live: switch profiles. Use application-prod.properties with a real database (PostgreSQL), enable logging, disable DevTools, set a robust servlet container (Tomcat embedded is already there). Add security with Spring Security (not covered here, but essential). And don't forget to configure CORS if you have a separate frontend.
Example PostgreSQL configuration:
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=user
spring.datasource.password=pass
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false
Use spring.jpa.hibernate.ddl-auto=validate in production – don't let Hibernate generate the schema unless you're in development.
In Summary – What to Do Now
- Generate the project on start.spring.io with the listed dependencies.
- Create entities and repositories for your domain.
- Write services with business logic, not in endpoints.
- Implement validation and error handling before building the UI.
- Test every layer – unit, integration, API with MockMvc.
- Configure profiles for development and production.
- Don't forget security and CORS – Spring Security is the next step.
You've just built a complete Spring Boot 3 web application. Now it's yours. Modify it, extend it, grow it. At Meteora Web, we use Spring Boot in real projects because it gives control, performance, and a solid community. If you want to dive deeper, check the official documentation.
Sponsored Protocol