How do I build a web application using Spring MVC?

Building a Web Application Using Spring MVC

A Spring MVC web application is typically built around these pieces:

  1. DispatcherServlet — the front controller that receives HTTP requests.
  2. Spring MVC configuration — enables MVC and configures controllers, view resolution, static resources, etc.
  3. Controllers — handle web requests.
  4. Services — contain business logic.
  5. Repositories — handle persistence, often with Spring Data JPA.
  6. Views or REST responses — return HTML pages, JSON, text, etc.
  7. 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:

  1. Add Spring MVC dependencies.
  2. Create an application entry point.
  3. Enable component scanning.
  4. Create controllers with @Controller or @RestController.
  5. Add services with @Service.
  6. Add repositories with Spring Data JPA if needed.
  7. Use constructor injection.
  8. Add validation with Jakarta Bean Validation.
  9. Configure views if returning HTML.
  10. Configure persistence if using a database.
  11. Add global exception handling.
  12. Write tests for controllers and services.
  13. 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.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.