Building a Web Application Using Spring MVC
A Spring MVC web application is typically built around these pieces:
- DispatcherServlet — the front controller that receives HTTP requests.
- Spring MVC configuration — enables MVC and configures controllers, view resolution, static resources, etc.
- Controllers — handle web requests.
- Services — contain business logic.
- Repositories — handle persistence, often with Spring Data JPA.
- Views or REST responses — return HTML pages, JSON, text, etc.
- Deployment setup — either Spring Boot embedded server or traditional WAR deployment.
1. Choose an Application Style
There are two common ways to build Spring MVC applications.
Option A: Spring Boot MVC Application
This is the most common modern approach.
You create an executable application with an embedded server such as Tomcat.
Option B: Traditional Spring MVC WAR Application
You deploy a WAR file to an external servlet container such as Tomcat.
Both use Spring MVC, but Spring Boot reduces configuration significantly.
Option A: Spring Boot + Spring MVC
2. Add Dependencies
If using Maven, a basic Spring MVC web application can start with:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Optional: for validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Optional: for JPA/database access -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
spring-boot-starter-web includes Spring MVC and an embedded servlet container.
3. Create the Main Application Class
package com.example.app;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WebApplication {
public static void main(String[] args) {
SpringApplication.run(WebApplication.class, args);
}
}
@SpringBootApplication enables component scanning, auto-configuration, and Spring configuration support.
Recommended package structure:
com.example.app
├── WebApplication.java
├── controller
│ └── HomeController.java
├── service
│ └── GreetingService.java
├── repository
│ └── UserRepository.java
└── model
└── User.java
Keep the main class in the root package so Spring can scan subpackages.
4. Create a REST Controller
For JSON/text responses, use @RestController.
package com.example.app.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloRestController {
@GetMapping("/api/hello")
public String hello() {
return "Hello from Spring MVC";
}
}
Run the application and visit:
http://localhost:8080/api/hello
5. Create an MVC Controller That Returns a View
If you want server-rendered HTML pages, use @Controller.
package com.example.app.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home(Model model) {
model.addAttribute("message", "Welcome to Spring MVC");
return "home";
}
}
With Thymeleaf, add:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
Then create:
src/main/resources/templates/home.html
<!DOCTYPE html>
<html>
<head>
<title>Spring MVC</title>
</head>
<body>
<h1 th:text="${message}">Default message</h1>
</body>
</html>
Spring Boot automatically configures Thymeleaf templates from src/main/resources/templates.
6. Add a Service Layer
Controllers should usually delegate business logic to services.
package com.example.app.service;
import org.springframework.stereotype.Service;
@Service
public class GreetingService {
public String getGreeting() {
return "Hello from the service layer";
}
}
Inject the service into a controller using constructor injection:
package com.example.app.controller;
import com.example.app.service.GreetingService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private final GreetingService greetingService;
public GreetingController(GreetingService greetingService) {
this.greetingService = greetingService;
}
@GetMapping("/api/greeting")
public String greeting() {
return greetingService.getGreeting();
}
}
7. Handle Form Data
For a traditional web form:
package com.example.app.controller;
import com.example.app.form.ContactForm;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class ContactController {
@GetMapping("/contact")
public String showForm(Model model) {
model.addAttribute("contactForm", new ContactForm());
return "contact";
}
@PostMapping("/contact")
public String submitForm(ContactForm contactForm, Model model) {
model.addAttribute("message", "Thanks, " + contactForm.getName());
return "contact-success";
}
}
Form object:
package com.example.app.form;
public class ContactForm {
private String name;
private String email;
private String message;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
With Lombok, this can be simplified:
package com.example.app.form;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ContactForm {
private String name;
private String email;
private String message;
}
8. Add Validation
Use Jakarta Bean Validation annotations:
package com.example.app.form;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ContactForm {
@NotBlank
private String name;
@Email
@NotBlank
private String email;
@NotBlank
private String message;
}
Controller:
package com.example.app.controller;
import com.example.app.form.ContactForm;
import jakarta.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class ContactController {
@PostMapping("/contact")
public String submitForm(
@Valid ContactForm contactForm,
BindingResult bindingResult
) {
if (bindingResult.hasErrors()) {
return "contact";
}
return "contact-success";
}
}
In Spring MVC, BindingResult must immediately follow the validated argument.
9. Add Persistence with Spring Data JPA
Entity:
package com.example.app.user;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
}
Repository:
package com.example.app.user;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
Service:
package com.example.app.user;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional(readOnly = true)
public List<User> findAll() {
return userRepository.findAll();
}
@Transactional
public User save(User user) {
return userRepository.save(user);
}
}
Controller:
package com.example.app.user;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class UserRestController {
private final UserService userService;
public UserRestController(UserService userService) {
this.userService = userService;
}
@GetMapping("/api/users")
public List<User> users() {
return userService.findAll();
}
}
Option B: Traditional Spring MVC Without Spring Boot
If you are building a classic Spring MVC application deployed as a WAR, you usually configure the application with Java configuration classes.
10. Add MVC Configuration
package com.example.app.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.example.app")
public class WebConfig implements WebMvcConfigurer {
}
@EnableWebMvc enables Spring MVC features such as request mapping, message conversion, validation support, and more.
11. Configure the DispatcherServlet
For a Servlet 3+ container, you can initialize Spring MVC without web.xml:
package com.example.app.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
Typical separation:
RootConfig -> services, repositories, data sources, transactions
WebConfig -> controllers, view resolvers, Spring MVC configuration
DispatcherServlet -> receives web requests
12. Add a Root Configuration
package com.example.app.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = {
"com.example.app.service",
"com.example.app.repository"
})
public class RootConfig {
}
13. Configure Views
For JSP views in a traditional Spring MVC app:
package com.example.app.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
public class ViewConfig {
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
}
Or add it directly to WebConfig:
package com.example.app.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.example.app.controller")
public class WebConfig implements WebMvcConfigurer {
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
}
Controller:
package com.example.app.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class PageController {
@GetMapping("/")
public String index(Model model) {
model.addAttribute("message", "Hello Spring MVC");
return "index";
}
}
JSP file:
src/main/webapp/WEB-INF/views/index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
<title>Spring MVC</title>
</head>
<body>
<h1>${message}</h1>
</body>
</html>
14. Recommended Layering
A clean Spring MVC application usually follows this flow:
HTTP Request
↓
DispatcherServlet
↓
Controller
↓
Service
↓
Repository
↓
Database
Example responsibilities:
| Layer | Annotation | Responsibility |
|---|---|---|
| Controller | @Controller, @RestController |
Handle HTTP requests/responses |
| Service | @Service |
Business logic and transactions |
| Repository | @Repository or Spring Data interface |
Data access |
| Entity/Model | @Entity, DTOs, form objects |
Data structure |
15. Basic REST Endpoint Example
package com.example.app.employee;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class EmployeeController {
@GetMapping("/employees")
public List<String> employees() {
return List.of("Alice", "Bob", "Charlie");
}
}
Calling:
GET http://localhost:8080/employees
returns JSON:
["Alice", "Bob", "Charlie"]
16. Common Spring MVC Annotations
| Annotation | Purpose |
|---|---|
@Controller |
MVC controller that usually returns a view name |
@RestController |
REST controller returning response bodies |
@RequestMapping |
General request mapping |
@GetMapping |
Handles HTTP GET |
@PostMapping |
Handles HTTP POST |
@PutMapping |
Handles HTTP PUT |
@DeleteMapping |
Handles HTTP DELETE |
@PathVariable |
Reads values from URI path |
@RequestParam |
Reads query/form parameters |
@RequestBody |
Reads JSON/XML request body |
@ResponseBody |
Writes method return value directly to response |
@ModelAttribute |
Binds form/model data |
@Valid |
Triggers Jakarta Bean Validation |
17. Example REST Controller with Request Body
DTO:
package com.example.app.employee;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public record CreateEmployeeRequest(
@NotBlank String name,
@Email @NotBlank String email
) {
}
Controller:
package com.example.app.employee;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class EmployeeController {
@PostMapping("/employees")
@ResponseStatus(HttpStatus.CREATED)
public String createEmployee(@Valid @RequestBody CreateEmployeeRequest request) {
return "Created employee: " + request.name();
}
}
Example request:
POST /employees HTTP/1.1
Content-Type: application/json
{
"name": "Alice",
"email": "[email protected]"
}
18. Handle Errors Globally
Use @ControllerAdvice for centralized exception handling.
package com.example.app.web;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleIllegalArgument(IllegalArgumentException exception) {
return new ErrorResponse(exception.getMessage());
}
public record ErrorResponse(String message) {
}
}
19. Test a Controller
With Spring Boot, you can test MVC endpoints using MockMvc:
package com.example.app.employee;
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;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(EmployeeController.class)
class EmployeeControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void employeesReturnsList() throws Exception {
mockMvc.perform(get("/employees"))
.andExpect(status().isOk())
.andExpect(content().json("[\"Alice\",\"Bob\",\"Charlie\"]"));
}
}
20. Practical Checklist
To build a Spring MVC web application:
- Add Spring MVC dependencies.
- Create an application entry point.
- Enable component scanning.
- Create controllers with
@Controlleror@RestController. - Add services with
@Service. - Add repositories with Spring Data JPA if needed.
- Use constructor injection.
- Add validation with Jakarta Bean Validation.
- Configure views if returning HTML.
- Configure persistence if using a database.
- Add global exception handling.
- Write tests for controllers and services.
- Run the application and test endpoints in a browser, curl, or Postman.
For most new applications, use Spring Boot with spring-boot-starter-web. For traditional servlet-container deployment, use Java config with @EnableWebMvc and a DispatcherServlet initializer.
