How Do I Build REST APIs with Spring MVC?

Building REST APIs with Spring MVC

In Spring MVC, you build REST APIs by defining controller classes that map HTTP requests to Java methods. In modern Spring Boot applications, this is usually done with @RestController.

A typical REST API is organized like this:

HTTP Request
    ↓
Controller
    ↓
Service
    ↓
Repository
    ↓
Database

Each layer has a clear responsibility:

Layer Responsibility
Controller Handles HTTP requests and responses
Service Contains business logic
Repository Handles database access
Entity Represents database tables
DTO Represents API request/response data

1. Add the Spring Web Dependency

For Maven:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

If you need validation:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

If you use JPA:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

2. Create a REST Controller

Use @RestController for REST APIs. It combines @Controller and @ResponseBody, meaning returned objects are written directly to the HTTP response, usually as JSON.

package com.example.demo.user;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/api/hello")
    public String hello() {
        return "Hello, REST API!";
    }
}

Calling:

GET /api/hello

returns:

Hello, REST API!

3. Design Resource-Based URLs

REST APIs should use nouns for resources and HTTP methods for actions.

Good:

GET    /api/users
GET    /api/users/1
POST   /api/users
PUT    /api/users/1
DELETE /api/users/1

Avoid action-style URLs like:

/api/getUsers
/api/createUser
/api/deleteUser

The HTTP method already describes the operation.


4. Create DTOs for Request and Response Bodies

Avoid exposing database entities directly from your API. Use DTOs instead.

package com.example.demo.user;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record CreateUserRequest(
        @NotBlank String name,
        @NotBlank @Email String email
) {
}
package com.example.demo.user;

public record UserResponse(
        Long id,
        String name,
        String email
) {
}

DTOs keep your API contract separate from your database model.


5. Create REST Endpoints

A controller for basic CRUD operations might look like this:

package com.example.demo.user;

import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public List<UserResponse> findAll() {
        return userService.findAll();
    }

    @GetMapping("/{id}")
    public UserResponse findById(@PathVariable Long id) {
        return userService.findById(id);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public UserResponse create(@Valid @RequestBody CreateUserRequest request) {
        return userService.create(request);
    }

    @PutMapping("/{id}")
    public UserResponse update(
            @PathVariable Long id,
            @Valid @RequestBody CreateUserRequest request
    ) {
        return userService.update(id, request);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) {
        userService.delete(id);
    }
}

Key annotations:

Annotation Purpose
@RestController Marks the class as a REST controller
@RequestMapping Defines a base URL
@GetMapping Handles HTTP GET
@PostMapping Handles HTTP POST
@PutMapping Handles HTTP PUT
@DeleteMapping Handles HTTP DELETE
@PathVariable Reads values from the URL path
@RequestBody Reads JSON from the request body
@Valid Triggers Jakarta Bean Validation
@ResponseStatus Sets the HTTP response status

6. Put Business Logic in a Service

Controllers should stay thin. Put business rules in a service class.

package com.example.demo.user;

import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {

    public List<UserResponse> findAll() {
        // Load users from repository
        return List.of();
    }

    public UserResponse findById(Long id) {
        // Find user by id
        return new UserResponse(id, "Alice", "[email protected]");
    }

    public UserResponse create(CreateUserRequest request) {
        // Create user
        return new UserResponse(1L, request.name(), request.email());
    }

    public UserResponse update(Long id, CreateUserRequest request) {
        // Update user
        return new UserResponse(id, request.name(), request.email());
    }

    public void delete(Long id) {
        // Delete user
    }
}

In a real application, the service would call a repository.


7. Use Spring Data JPA for Persistence

If your API stores data in a database, create an entity and repository.

package com.example.demo.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;
}
package com.example.demo.user;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}

By extending JpaRepository, you automatically get methods like:

findAll();
findById(id);
save(entity);
delete(entity);
existsById(id);

8. Return Proper HTTP Status Codes

Use status codes that match the result:

Situation Status
Successful read 200 OK
Created resource 201 Created
Deleted resource 204 No Content
Invalid request 400 Bad Request
Missing resource 404 Not Found
Conflict 409 Conflict
Server error 500 Internal Server Error

For creation, you can also return a Location header:

package com.example.demo.user;

import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping
    public ResponseEntity<UserResponse> create(
            @Valid @RequestBody CreateUserRequest request,
            UriComponentsBuilder uriBuilder
    ) {
        UserResponse response = userService.create(request);

        URI location = uriBuilder
                .path("/api/users/{id}")
                .buildAndExpand(response.id())
                .toUri();

        return ResponseEntity.created(location).body(response);
    }
}

9. Handle Errors Globally

Use @RestControllerAdvice to return consistent JSON errors.

package com.example.demo.exception;

import java.time.Instant;
import java.util.List;

public record ApiError(
        int status,
        String error,
        String message,
        String path,
        Instant timestamp,
        List<FieldErrorDetail> fieldErrors
) {
    public record FieldErrorDetail(
            String field,
            String message
    ) {
    }
}
package com.example.demo.exception;

public class ResourceNotFoundException extends RuntimeException {

    public ResourceNotFoundException(String message) {
        super(message);
    }
}
package com.example.demo.exception;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

import java.time.Instant;
import java.util.List;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ApiError handleNotFound(
            ResourceNotFoundException ex,
            HttpServletRequest request
    ) {
        return new ApiError(
                404,
                "Not Found",
                ex.getMessage(),
                request.getRequestURI(),
                Instant.now(),
                List.of()
        );
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiError handleValidation(
            MethodArgumentNotValidException ex,
            HttpServletRequest request
    ) {
        List<ApiError.FieldErrorDetail> fieldErrors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(error -> new ApiError.FieldErrorDetail(
                        error.getField(),
                        error.getDefaultMessage()
                ))
                .toList();

        return new ApiError(
                400,
                "Bad Request",
                "Validation failed",
                request.getRequestURI(),
                Instant.now(),
                fieldErrors
        );
    }
}

10. Test Your API

Example using curl:

curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","email":"[email protected]"}'

Get all users:

curl http://localhost:8080/api/users

Get one user:

curl http://localhost:8080/api/users/1

Update a user:

curl -X PUT http://localhost:8080/api/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice Smith","email":"[email protected]"}'

Delete a user:

curl -X DELETE http://localhost:8080/api/users/1

11. Add Pagination for List Endpoints

For large collections, avoid returning everything at once.

package com.example.demo.user;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public Page<UserResponse> findAll(Pageable pageable) {
        return userService.findAll(pageable);
    }
}

Then clients can call:

GET /api/users?page=0&size=10
GET /api/users?page=0&size=10&sort=name,asc

Recommended Checklist

When building REST APIs with Spring MVC:

  • Use @RestController.
  • Use resource-based URLs like /api/users.
  • Use HTTP methods correctly: GET, POST, PUT, PATCH, DELETE.
  • Keep controllers thin.
  • Put business logic in services.
  • Use repositories for database access.
  • Use DTOs instead of exposing entities.
  • Validate request bodies with jakarta.validation.
  • Handle errors globally with @RestControllerAdvice.
  • Return correct HTTP status codes.
  • Add pagination for collection endpoints.
  • Use jakarta.* imports in modern Spring Boot applications.

A clean REST API usually follows this shape:

Controller → Service → Repository → Database

That structure keeps your Spring MVC API easier to test, maintain, and evolve.

How do I handle exceptions globally in Spring MVC?

In Spring MVC, handle exceptions globally by creating a class annotated with @ControllerAdvice or @RestControllerAdvice and adding methods annotated with @ExceptionHandler.

For REST APIs, prefer @RestControllerAdvice, because it combines @ControllerAdvice and @ResponseBody, so returned objects are serialized as JSON automatically.

package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFoundException(
            ResourceNotFoundException ex
    ) {
        ErrorResponse error = new ErrorResponse(
                HttpStatus.NOT_FOUND.value(),
                ex.getMessage()
        );

        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(
            Exception ex
    ) {
        ErrorResponse error = new ErrorResponse(
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                "An unexpected error occurred"
        );

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

Example error response DTO:

package com.example.demo.exception;

import java.time.Instant;

public record ErrorResponse(
        int status,
        String message,
        Instant timestamp
) {
    public ErrorResponse(int status, String message) {
        this(status, message, Instant.now());
    }
}

Example custom exception:

package com.example.demo.exception;

public class ResourceNotFoundException extends RuntimeException {

    public ResourceNotFoundException(String message) {
        super(message);
    }
}

Then you can throw exceptions from controllers or services:

throw new ResourceNotFoundException("User not found");

Spring will automatically route that exception to the matching @ExceptionHandler.

Common handlers you may want to add:

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
        MethodArgumentNotValidException ex
) {
    ErrorResponse error = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "Validation failed"
    );

    return ResponseEntity.badRequest().body(error);
}

Use:

  • @RestControllerAdvice for REST APIs returning JSON.
  • @ControllerAdvice for MVC apps returning views or when you manually use ResponseEntity.
  • Specific exception handlers before generic ones.
  • A final @ExceptionHandler(Exception.class) as a fallback.

How do I validate form data in Spring?

In Spring MVC, the standard way to validate form data is to use Jakarta Bean Validation annotations on a form/DTO object, then check validation results in your controller with BindingResult.

Since your project uses Jakarta EE, use jakarta.validation.* imports.

1. Add validation annotations to your form object

Example form/DTO:

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public class UserForm {

    @NotBlank(message = "Name is required")
    @Size(max = 100, message = "Name must be at most 100 characters")
    private String name;

    @NotBlank(message = "Email is required")
    @Email(message = "Please enter a valid email address")
    private String email;

    @NotBlank(message = "Password is required")
    @Size(min = 8, message = "Password must be at least 8 characters")
    private String password;

    // getters and setters
}

With Lombok:

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class UserForm {

    @NotBlank(message = "Name is required")
    @Size(max = 100, message = "Name must be at most 100 characters")
    private String name;

    @NotBlank(message = "Email is required")
    @Email(message = "Please enter a valid email address")
    private String email;

    @NotBlank(message = "Password is required")
    @Size(min = 8, message = "Password must be at least 8 characters")
    private String password;
}

2. Use @Valid in your controller

import jakarta.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class UserController {

    @GetMapping("/register")
    public String showRegisterForm(Model model) {
        model.addAttribute("userForm", new UserForm());
        return "register";
    }

    @PostMapping("/register")
    public String register(
            @Valid UserForm userForm,
            BindingResult bindingResult
    ) {
        if (bindingResult.hasErrors()) {
            return "register";
        }

        // Save user or call service layer here
        return "redirect:/register/success";
    }
}

Important: BindingResult must come immediately after the validated object.

Correct:

public String register(@Valid UserForm userForm, BindingResult bindingResult)

Incorrect:

public String register(@Valid UserForm userForm, Model model, BindingResult bindingResult)

3. Display errors in Thymeleaf

If you use Thymeleaf:

<form th:action="@{/register}" th:object="${userForm}" method="post">
    <div>
        <label>Name</label>
        <input type="text" th:field="*{name}">
        <span th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></span>
    </div>

    <div>
        <label>Email</label>
        <input type="email" th:field="*{email}">
        <span th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></span>
    </div>

    <div>
        <label>Password</label>
        <input type="password" th:field="*{password}">
        <span th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></span>
    </div>

    <button type="submit">Register</button>
</form>

4. Common validation annotations

@NotNull
@NotBlank
@NotEmpty
@Size(min = 2, max = 100)
@Min(18)
@Max(120)
@Email
@Pattern(regexp = "...")
@Past
@Future
@Positive
@PositiveOrZero

Use:

  • @NotNull for any value that must not be null
  • @NotBlank for strings that must contain non-whitespace text
  • @NotEmpty for strings, collections, arrays, or maps that must not be empty
  • @Size for string length or collection size
  • @Email for email format validation

5. Maven dependency

If you use Spring Boot, add:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

For Gradle:

implementation("org.springframework.boot:spring-boot-starter-validation")

6. Service-layer validation

You can also validate method parameters in Spring services:

import jakarta.validation.Valid;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

@Service
@Validated
public class UserService {

    public void createUser(@Valid UserForm userForm) {
        // business logic
    }
}

7. REST API validation example

For JSON request bodies:

import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserRestController {

    @PostMapping("/api/users")
    public String createUser(@Valid @RequestBody UserForm userForm) {
        return "User created";
    }
}

For REST APIs, invalid input usually results in a 400 Bad Request.

Summary

Use this pattern:

@PostMapping("/submit")
public String submit(@Valid MyForm form, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        return "form-page";
    }

    return "redirect:/success";
}

That is the typical Spring MVC form validation flow.

How do I handle HTTP requests with Spring controllers?

In Spring MVC or Spring Boot, you handle HTTP requests by creating controller classes. A controller receives a request, runs application logic, and returns either:

  • a view name for server-rendered pages, or
  • data such as JSON for REST APIs.

1. Basic Spring MVC Controller

Use @Controller when you want to return views such as JSP, Thymeleaf, or other templates.

package com.example.web;

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";
    }
}

In this example:

  • @Controller marks the class as a Spring MVC controller.
  • @GetMapping("/") handles HTTP GET /.
  • Model passes data to the view.
  • "home" is the logical view name.

If you use Thymeleaf, Spring would typically look for:

src/main/resources/templates/home.html

2. REST Controller Returning JSON

Use @RestController when you want to build REST APIs.

package com.example.web;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingRestController {

    @GetMapping("/api/greeting")
    public Greeting greeting() {
        return new Greeting("Hello from Spring!");
    }

    public record Greeting(String message) {
    }
}

Calling:

GET /api/greeting

returns JSON like:

{
  "message": "Hello from Spring!"
}

@RestController is a shortcut for:

@Controller
@ResponseBody

So every method returns the response body directly instead of a view name.


3. Handling Different HTTP Methods

Spring provides convenient annotations for common HTTP methods.

package com.example.web;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @GetMapping("/users")
    public String getUsers() {
        return "Get all users";
    }

    @PostMapping("/users")
    public String createUser() {
        return "Create a new user";
    }

    @PutMapping("/users/1")
    public String replaceUser() {
        return "Replace user";
    }

    @PatchMapping("/users/1")
    public String updateUser() {
        return "Update part of user";
    }

    @DeleteMapping("/users/1")
    public String deleteUser() {
        return "Delete user";
    }
}

Common mappings include:

Annotation HTTP Method
@GetMapping GET
@PostMapping POST
@PutMapping PUT
@PatchMapping PATCH
@DeleteMapping DELETE

4. Reading Path Variables

Use @PathVariable to read values from the URL path.

package com.example.web;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductController {

    @GetMapping("/products/{id}")
    public String getProduct(@PathVariable Long id) {
        return "Product ID: " + id;
    }
}

Request:

GET /products/10

Response:

Product ID: 10

5. Reading Query Parameters

Use @RequestParam to read query string parameters.

package com.example.web;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SearchController {

    @GetMapping("/search")
    public String search(
            @RequestParam String keyword,
            @RequestParam(defaultValue = "1") int page
    ) {
        return "Searching for: " + keyword + ", page: " + page;
    }
}

Request:

GET /search?keyword=spring&page=2

Response:

Searching for: spring, page: 2

6. Reading Request Body JSON

Use @RequestBody to bind JSON request data to a Java object.

package com.example.web;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

public record CreateUserRequest(String name, String email) {
}

@RestController
public class UserApiController {

    @PostMapping("/api/users")
    public String createUser(@RequestBody CreateUserRequest request) {
        return "Created user: " + request.name() + " with email: " + request.email();
    }
}

Request:

POST /api/users
Content-Type: application/json
{
  "name": "Alice",
  "email": "[email protected]"
}

7. Returning Proper HTTP Status Codes

Use ResponseEntity when you need control over the response status, headers, or body.

package com.example.web;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    @PostMapping("/orders")
    public ResponseEntity<String> createOrder() {
        return ResponseEntity
                .status(HttpStatus.CREATED)
                .body("Order created");
    }
}

This returns:

HTTP/1.1 201 Created

8. Handling Form Submissions

For traditional web applications, a controller can handle form submissions with @PostMapping.

package com.example.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class ContactController {

    @PostMapping("/contact")
    public String submitContactForm(
            @RequestParam String name,
            @RequestParam String message
    ) {
        System.out.println("Name: " + name);
        System.out.println("Message: " + message);

        return "redirect:/contact-success";
    }
}

The redirect prevents duplicate form submissions if the user refreshes the page.


9. Using a Service from a Controller

Controllers should usually be thin. Put business logic in a service class.

package com.example.web;

import com.example.service.UserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/api/users/count")
    public int countUsers() {
        return userService.countUsers();
    }
}
package com.example.service;

import org.springframework.stereotype.Service;

@Service
public class UserService {

    public int countUsers() {
        return 5;
    }
}

This keeps the controller focused on HTTP request/response handling.


10. Simple Exception Handling

You can handle exceptions globally using @ControllerAdvice.

package com.example.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 handleIllegalArgumentException(IllegalArgumentException ex) {
        return new ErrorResponse(ex.getMessage());
    }

    public record ErrorResponse(String message) {
    }
}

Now if a controller throws:

throw new IllegalArgumentException("Invalid request");

Spring returns a 400 Bad Request response.


Quick Summary

Use these annotations to handle HTTP requests in Spring:

Annotation Purpose
@Controller Web controller returning views
@RestController REST controller returning response bodies
@GetMapping Handle GET requests
@PostMapping Handle POST requests
@PutMapping Handle PUT requests
@PatchMapping Handle PATCH requests
@DeleteMapping Handle DELETE requests
@PathVariable Read values from the URL path
@RequestParam Read query parameters or form fields
@RequestBody Read JSON/XML request body
ResponseEntity Customize status, headers, and body
@ControllerAdvice Centralized exception handling

In short:

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello, Spring!";
    }
}

That is the simplest form of handling an HTTP request with a Spring controller.

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.

How do I map a request to a controller in Spring MVC?

In Spring MVC, you can map a request to a controller using the @RequestMapping (or aliases like @GetMapping, @PostMapping, etc.) annotation on a method. These annotations define a specific URL path and HTTP method that the method will handle. Here’s how you can do it:

Step-by-Step Instructions:

  1. Annotate the Class as a Controller: Use the @Controller annotation (or @RestController for REST APIs) on the class to indicate that it is a controller in your Spring application.
  2. Map URLs with @RequestMapping: Use the @RequestMapping annotation on methods to specify the URL patterns you want to map HTTP requests to. You can also use HTTP method-specific annotations, such as @GetMapping and @PostMapping.

Example

package org.kodejava.app;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/my-controller") // Base path for all endpoints in the controller
public class MyController {

    // Handles GET requests to /my-controller/hello
    @GetMapping("/hello")
    @ResponseBody
    public String sayHello() {
        return "Hello, World!";
    }

    // Handles POST requests to /my-controller/data
    @PostMapping("/data")
    @ResponseBody
    public String handleDataSubmission() {
        return "Data submitted successfully!";
    }
}

Key Annotations:

  1. @Controller: Marks the class as a Spring MVC controller.
  2. @RequestMapping: Used to map web requests onto specific methods or classes. It can be used with a combination of HTTP methods, URL patterns, and request parameters.
    @RequestMapping(value = "/example", method = RequestMethod.GET)
    
  3. HTTP Method-Specific Annotations: These are shorthand annotations for specific HTTP methods:
    • @GetMapping for GET requests
    • @PostMapping for POST requests
    • @PutMapping for PUT requests
    • @DeleteMapping for DELETE requests
    • @PatchMapping for PATCH requests

Notes:

  1. Class-Level @RequestMapping: If you specify a base path with @RequestMapping at the class level, all methods will inherit this as part of their path.
  2. Return Types:
    • Use @ResponseBody to return plain text, JSON, or XML directly in the HTTP response body.
    • If you are returning a view name, you don’t need @ResponseBody.
  3. Path Variables and Query Parameters: You can also handle dynamic path variables using the @PathVariable and request parameters using the @RequestParam annotations.

Example with Path Variables and Request Parameters:

package org.kodejava.app;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
public class UserController {

    // Dynamic path variable
    @GetMapping("/{id}")
    public String getUserById(@PathVariable("id") Long id) {
        return "User ID: " + id;
    }

    // Query parameter
    @GetMapping("/search")
    public String searchUsers(@RequestParam("name") String name) {
        return "Searching for user: " + name;
    }
}

By following this pattern, you can map requests to specific controller methods and build robust web applications with Spring MVC.

How do I configure Spring MVC using Java-based configuration?

To configure Spring MVC using a Java-based configuration, you can follow these steps:

  1. Enable Spring MVC support by using the @EnableWebMvc annotation.
  2. Create a Spring configuration class annotated with @Configuration.
  3. Configure the component scanning to detect controllers, services, and other components.
  4. Define a ViewResolver bean to map view names to actual views (e.g., JSPs, Thymeleaf templates, etc.).
  5. Set up other essential configurations like static resource handling, CORS, or message converters if necessary.

Here’s an example configuration class:

package org.kodejava.demo;

import org.springframework.context.annotation.Bean;
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.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "org.kodejava.demo") // Specify your base package where controllers are located
public class WebConfig implements WebMvcConfigurer {

    // Define a ViewResolver bean
    @Bean
    public InternalResourceViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/views/"); // Path to your view templates
        viewResolver.setSuffix(".jsp"); // View file extension (e.g., .jsp or .html)
        return viewResolver;
    }

    // Static resource handling (e.g., for serving CSS, JS, images, etc.)
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/resources/");
    }
}

Step-by-step Explanation:

  1. @EnableWebMvc:
    • This annotation imports Spring MVC configuration from WebMvcConfigurationSupport, which enables features like DispatcherServlet, handler mappings, and more.
  2. @ComponentScan:
    • This annotation configures Spring to scan the specified package(s) for components, such as controllers, services, and repositories.
  3. Define a ViewResolver:
    • In the example, InternalResourceViewResolver maps view names to JSP files located under /WEB-INF/views/ with the .jsp extension.
  4. Static Resources:
    • The addResourceHandlers method maps requests for static resources (e.g., CSS, JS, images) to physical locations.

Setting Up the DispatcherServlet

You’ll also need to configure the DispatcherServlet in your file (if you’re using a traditional deployment structure) or via a programmatic initializer (preferred in modern setups). web.xml
Here’s an example of a Java-based initializer:

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[] { RootConfig.class }; // Configuration for application-wide beans (e.g., data sources)
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[] { WebConfig.class }; // Configuration for Spring MVC
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" }; // Map all requests to DispatcherServlet
    }
}

Explanation of the Java-Based Initializer:

  • getRootConfigClasses:
    • Specifies configuration for the root application context, such as services, persistence, or security.
  • getServletConfigClasses:
    • Specifies configuration for the Spring MVC child context (e.g., controllers, view resolvers).
  • getServletMappings:
    • Maps the DispatcherServlet to handle requests starting at . /

With these steps, you’ll have a fully functional Spring MVC configuration using Java-based configuration.

How do I create a simple Hello World web app using Spring MVC?

Creating a simple “Hello World” web application using Spring MVC involves several steps. Below is a guide that will walk you through creating the application from scratch:


1. Set Up the Project

Use a build tool like Maven or Gradle to manage your dependencies. Here, we’ll use Maven.

pom.xml

Create a pom.xml file and include the necessary dependencies:

<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.kodejava</groupId>
    <artifactId>hello-world-spring-mvc</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <dependencies>
        <!-- Spring MVC -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>6.1.0</version>
        </dependency>

        <!-- Spring Boot Starter (Optional for running the app with Boot) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>3.2.0</version>
        </dependency>

        <!-- JSTL for view rendering -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

        <!-- Servlet API -->
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>5.0.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

2. Configure the Project

web.xml

If you’re using traditional deployment (instead of Spring Boot), a web.xml configuration file is required under src/main/webapp/WEB-INF/.

<web-app xmlns="http://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://jakarta.ee/xml/ns/jakartaee http://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">
    <display-name>Hello World App</display-name>

    <!-- DispatcherServlet mapping -->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!-- Context loader -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/dispatcher-servlet.xml</param-value>
    </context-param>
</web-app>

3. Spring Configuration

dispatcher-servlet.xml (Spring MVC Application Context)

Place this file under WEB-INF/.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans 
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context 
           http://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/mvc 
           http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- Enable annotation-based Spring components -->
    <context:component-scan base-package="org.kodejava" />
    <mvc:annotation-driven />

    <!-- Configure the view resolver -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/" />
        <property name="suffix" value=".jsp" />
    </bean>
</beans>

4. Creating the Controller

HelloWorldController.java

Create a controller in the org.kodejava package.

package org.kodejava;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.ui.Model;

@Controller
public class HelloWorldController {

    @GetMapping("/")
    public String helloWorld(Model model) {
        model.addAttribute("message", "Hello, World!");
        return "hello"; // Refers to the hello.jsp view
    }
}

5. Create a View

hello.jsp

Place the JSP file in src/main/webapp/WEB-INF/views/.

<!DOCTYPE html>
<html>
<head>
    <title>Hello World</title>
</head>
<body>
    <h1>${message}</h1>
</body>
</html>

6. Running the Application

Using Spring Boot (Simpler)

  • If you added Spring Boot dependencies in your pom.xml, include a main method and run the project:
package org.kodejava;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HelloWorldApplication {
    public static void main(String[] args) {
        SpringApplication.run(HelloWorldApplication.class, args);
    }
}

Using a Traditional Servlet Container

  • Package your application as a WAR file (mvn clean package) and deploy it to a servlet container (e.g., Apache Tomcat).

Access the Application

Once the application is deployed, access the URL: http://localhost:8080/

You will see the Hello, World! message displayed.