Spring MVC is the web module of the Spring Framework, and we use it to handle HTTP requests in controller classes. It follows the Model-View-Controller pattern, so a controller method prepares the data in a model and picks the view that renders the response.
We use Spring MVC in two ways. It renders HTML pages on the server with a template engine such as Thymeleaf, and it serves REST APIs that return JSON to a JavaScript app or a mobile app. Spring Boot sets it up for us, together with an embedded Tomcat server.
The following example is a small hello world application built with Spring Boot 4.1.1. It has one controller that maps the URL /hello and one Thymeleaf template that prints a greeting. We add two starters to pom.xml, and Spring Boot sets up the rest.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId> <!-- Spring MVC + Tomcat -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId> <!-- HTML templates -->
</dependency>
@Controller
public class HelloController {
@GetMapping("/hello")
public String hello(@RequestParam(defaultValue = "World") String name, Model model) {
model.addAttribute("message", "Hello, " + name + "!");
return "hello"; // view name -> src/main/resources/templates/hello.html
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Hello Page</title>
<link rel="stylesheet" th:href="@{/css/style.css}">
</head>
<body>
<h1 th:text="*{message}">Placeholder message</h1>
</body>
</html>
mvn spring-boot:run
curl http://localhost:8080/hello # <h1>Hello, World!</h1>
curl "http://localhost:8080/hello?name=Lokesh" # <h1>Hello, Lokesh!</h1>
Notice that both calls use the same template. Only the message value in the model changes.
Next, we see how the DispatcherServlet routes each request and what Spring Boot configures for us. After that, we build the example step by step with @RestController, values from the URL, CSS files and MockMvc tests, and fix the first errors that beginners meet.
1. What Is Spring MVC?
Spring MVC runs on servlets (Java classes that a web server such as Tomcat calls for each HTTP request) and follows the Model-View-Controller (MVC) pattern, which keeps the data and the HTML apart from the code that handles the request.
Each MVC part maps to one piece of our hello world application.
| MVC part | Job | In our application |
|---|---|---|
| Model | Holds the data the page shows | The Model object with the attribute message = “Hello, Lokesh!” |
| View | Turns the data into HTML | The Thymeleaf template hello.html |
| Controller | Reads the request, prepares the model, picks the view | HelloController.hello() |
The controller returns only a view name, not the HTML. For example, in an online bookstore, a designer can change the layout of the product page in the template while a developer changes the discount logic in the controller, and neither change touches the other’s file.
1.1. How a Request Flows Through the DispatcherServlet
Every request to a Spring MVC application first reaches one servlet, the DispatcherServlet, which is the single entry point for all requests. The DispatcherServlet passes each request to the right controller method and then passes the result to the right view. A single entry point like this is called a front controller.

The steps in the diagram are the same for every @Controller request.
- The browser sends GET /hello?name=Lokesh to the DispatcherServlet.
- A HandlerMapping bean (a bean is an object that Spring creates and manages) reads the @GetMapping annotation and picks HelloController.hello() for the URL.
- The DispatcherServlet calls hello() and fills the parameters, so the name value comes from the query string and the model is a new, empty Model.
- The method adds message to the model and returns the view name “hello”.
- The ThymeleafViewResolver turns “hello” into a file path by adding the prefix templates/ and the suffix .html, so the result is classpath:/templates/hello.html.
- Thymeleaf renders the template with the model values.
- The DispatcherServlet writes the HTML to the response.
1.2. What Spring Boot Configures for Us
In a Spring MVC application without Spring Boot, we create the DispatcherServlet, a view resolver and a template engine ourselves. With Spring Boot, the two starters are enough. Spring Boot sees the libraries on the classpath and creates these beans for us, a feature called auto-configuration. With the two starters, Spring Boot creates five parts.
- A DispatcherServlet for all URLs, running on embedded Tomcat 11 at port 8080. We can change the port with server.port.
- A ThymeleafViewResolver that reads templates from src/main/resources/templates, with the .html suffix.
- A handler for static files in src/main/resources/static, so the file static/index.html becomes the welcome page for “/”.
- An /error endpoint that shows the Whitelabel error page when we have no error page of our own.
- Jackson, the library that turns Java objects into JSON for @RestController methods.
We put the @SpringBootApplication annotation on the main class. It turns on auto-configuration and scans the package of the main class, which is where Spring finds our controllers.
1.3. @Controller vs @RestController
Both annotations mark a class as a web controller, and the difference is what happens to the return value. A @Controller method returns a view name, and a template builds the page. A @RestController is @Controller plus @ResponseBody, so Spring writes the return value itself into the response, and for an object, Spring writes JSON.
@RestController
public class HelloRestController {
public record Greeting(String message) {
}
@GetMapping("/api/hello")
public Greeting hello(@RequestParam(defaultValue = "World") String name) {
return new Greeting("Hello, " + name + "!"); // {"message":"Hello, Lokesh!"}
}
}
HTTP/1.1 200
Content-Type: application/json
Content-Length: 28
{"message":"Hello, Lokesh!"}
In daily work, the two controllers differ in the meaning of the return value and in the response type.
| @Controller | @RestController | |
|---|---|---|
| Return value “hello” means | View name, rendered by Thymeleaf | The text hello in the response body |
| Typical response | text/html page | application/json data |
| Needs a template engine | Yes | No |
| Used for | Server-rendered web pages | REST APIs for JavaScript apps and other services |
A typical project uses both. The admin pages of an online shop are server-rendered with @Controller, whereas the mobile app reads the same product data as JSON from a @RestController.
Both styles can also live in one class, with @ResponseBody on some methods of a @Controller.
2. Spring MVC Hello World Example
The example runs on Spring Boot 4.1.1 with Java 25, and Maven picks Spring Framework 7.0.9, Thymeleaf 3.1.5, Tomcat 11.0.24 and Jackson 3.1.5. The complete project on GitHub includes 9 tests for the controllers.
2.1. Creating the Project
The quickest start is Spring Initializr, where we choose Maven, Java 25 and Spring Boot 4.1.1, and then add the “Spring Web” and “Thymeleaf” dependencies. The generated pom.xml uses spring-boot-starter-parent as its parent, which sets the versions of all Spring libraries, so we do not write versions ourselves.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
</parent>
<properties>
<java.version>25</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
In Spring Boot 4, the Spring MVC starter is spring-boot-starter-webmvc. The old name spring-boot-starter-web still works in 4.1.1, but its POM calls the starter “deprecated in favor of spring-boot-starter-webmvc”. The project keeps the Java code apart from the templates and the static files.
spring-boot-mvc-hello-world
|-- pom.xml
`-- src
|-- main
| |-- java/com/howtodoinjava/app
| | |-- App.java (main class)
| | `-- web
| | |-- HelloController.java (@Controller)
| | `-- HelloRestController.java (@RestController)
| `-- resources
| |-- application.properties
| |-- static
| | |-- index.html (welcome page)
| | `-- css/style.css
| `-- templates
| `-- hello.html (Thymeleaf view)
`-- test/java/com/howtodoinjava/app (MockMvc and HTTP tests)
2.2. Reading Request Values With @RequestParam and @PathVariable
A controller method gets values from the request through annotated parameters. @RequestParam reads a value from the query string, such as ?name=Lokesh, whereas @PathVariable reads a part of the URL path, such as Lokesh in /hello/Lokesh. Our controller supports both.
@GetMapping("/hello")
public String hello(@RequestParam(defaultValue = "World") String name, Model model) {
model.addAttribute("message", "Hello, " + name + "!");
return "hello";
}
@GetMapping("/hello/{name}")
public String helloPath(@PathVariable String name, Model model) {
model.addAttribute("message", "Hello, " + name + "!");
return "hello";
}
All three URLs use the same template, and only the message changes.
| URL | Method | message in the model |
|---|---|---|
| /hello | hello(), default value used | Hello, World! |
| /hello?name=Lokesh | hello() | Hello, Lokesh! |
| /hello/Lokesh | helloPath() | Hello, Lokesh! |
When we call /hello without ?name= and without a defaultValue, the request fails with status 400 (Bad Request), and the log says “Required request parameter ‘name’ for method parameter type String is not present”. This often happens when a link in an email or an old bookmark drops the query string. Without defaultValue, a @RequestParam is required.
Both annotations also support optional values and type conversion, and a @RequestMapping can match by HTTP method, headers and media types.
2.3. Writing the Thymeleaf Template
A Thymeleaf template is a plain HTML file with extra th: attributes. The file name must match the view name, so for the view name “hello” the file is src/main/resources/templates/hello.html (the full file is in the intro). The template uses three Thymeleaf attributes.
- The attribute xmlns:th=”http://www.thymeleaf.org” declares the th: prefix, so IDEs do not mark the attributes as errors.
- The attribute th:text=”*{message}“ puts the model attribute message into the h1 element and replaces the text “Placeholder message”. Most Thymeleaf examples write this with a dollar sign instead of the star, and here both forms give the same result, because no parent element sets a th:object.
- The attribute th:href=”@{/css/style.css}” builds the CSS link starting from the application context path (the URL prefix of the application), so the link still works when the application runs under a path such as /shop.
The text “Placeholder message” shows up only when we open the file straight from the disk in a browser, because when the application serves the page, Thymeleaf replaces the text.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello Page</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<h1>Hello, Lokesh!</h1>
</body>
</html>
Thymeleaf is the default choice for Spring Boot web pages. Other template engines such as FreeMarker and Mustache work with the same controller code.
2.4. Serving Static Resources
CSS, JavaScript and image files do not need a controller, because Spring Boot serves every file in src/main/resources/static as it is. The URL path is the same as the file path inside static, so static/css/style.css is served at /css/style.css.
body { font-family: Arial, sans-serif; margin: 40px; }
h1 { color: #1565c0; }
HTTP/1.1 200
Last-Modified: Sat, 03 Oct 2026 23:18:28 GMT
Accept-Ranges: bytes
Content-Type: text/css
Spring Boot also looks in classpath:/public/, classpath:/resources/ and classpath:/META-INF/resources/, but one static folder keeps the project layout clear. A file named static/index.html becomes the welcome page for “/”, as the startup log confirms.
o.s.b.w.a.WelcomePageHandlerMapping : Adding welcome page: class path resource [static/index.html]
Templates and static files go in different folders. Spring sends a file in static as it is, so Thymeleaf never processes th: attributes in that folder. A file in templates, on the other hand, is never sent to the browser as a file, and only a controller that returns its name can show it.
A common mistake is to copy hello.html into static and open /hello.html, so the browser shows “Placeholder message” instead of the greeting.
2.5. Running the Application
The main class starts Spring Boot and the embedded Tomcat server.
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
We can start the application from the IDE by running App.main(), or with Maven from the project folder.
mvn spring-boot:run
:: Spring Boot :: (v4.1.1)
o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
o.s.b.w.a.WelcomePageHandlerMapping : Adding welcome page: class path resource [static/index.html]
o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
com.howtodoinjava.app.App : Started App in 2.902 seconds (process running for 3.354)
When we open http://localhost:8080/hello?name=Lokesh in a browser, the page shows the heading “Hello, Lokesh!” in blue, styled by style.css. With curl, we see the status line and the HTML.
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Language: en-US
Transfer-Encoding: chunked
<!DOCTYPE html>
...
<h1>Hello, Lokesh!</h1>
...
To build a JAR file, we run mvn package, which creates target/spring-boot-mvc-hello-world-0.0.1-SNAPSHOT.jar, and running java -jar on that file starts the same server. During development, Spring Boot DevTools restarts the application after each code change.
2.6. Testing the Controller With MockMvc
MockMvc is a test helper that sends requests to the DispatcherServlet inside the test, without starting Tomcat. @WebMvcTest loads only the web parts (controllers, view resolvers and Thymeleaf), so the test starts in under a second. The class HelloControllerTest checks the view name and the model attribute, and it also checks the rendered HTML.
@WebMvcTest(HelloController.class)
class HelloControllerTest {
@Autowired
MockMvc mockMvc;
@Test
void helloRendersTheHelloView() throws Exception {
mockMvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(view().name("hello"))
.andExpect(model().attribute("message", "Hello, World!"))
.andExpect(content().string(containsString("<h1>Hello, World!</h1>")));
}
@Test
void pathVariableChangesTheName() throws Exception {
mockMvc.perform(get("/hello/Lokesh"))
.andExpect(view().name("hello"))
.andExpect(model().attribute("message", "Hello, Lokesh!"));
}
}
In Spring Boot 4, @WebMvcTest and @AutoConfigureMockMvc come from the spring-boot-starter-webmvc-test starter, in the package org.springframework.boot.webmvc.test.autoconfigure. Many Spring Boot 3 examples import org.springframework.boot.test.autoconfigure.web.servlet, and that old import no longer compiles. With MockMvc, we can also mock services with @MockitoBean and send POST requests.
3. Spring MVC Hello World FAQs
3.1. Why Do I Get a Whitelabel Error Page With Status 404?
The request matched no controller method and no static file. Because a browser sends the header Accept: text/html, Spring Boot shows its default error page, whereas curl does not send that header, so it gets JSON.
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Sun Oct 04 04:49:36 IST 2026
There was an unexpected error (type=Not Found, status=404).
{"timestamp":"2026-10-03T23:19:36.970Z","status":404,"error":"Not Found","path":"/helo"}
The usual causes take a minute to check.
- The URL does not match the @GetMapping path, for example /helo instead of /hello.
- The controller class is outside the package of the @SpringBootApplication class, for example, in com.howtodoinjava.web while the main class is in com.howtodoinjava.app. Spring scans only the package of the main class and the packages below it, so it never finds the controller.
- The class has no @Controller or @RestController annotation.
- The request uses another HTTP method, such as POST to a @GetMapping method, which returns 405, not 404.
3.2. Why Does a Missing Template Return a 500 Error?
When the controller returns a view name but templates has no file with that name, Thymeleaf cannot build the page, so the browser gets the Whitelabel page with status 500 and the log names the missing view. For example, a method that returns “missing-page” without a matching template produces a TemplateInputException in the log.
org.thymeleaf.exceptions.TemplateInputException: Error resolving template [missing-page], template might not exist or might not be accessible by any of the configured Template Resolvers
Whitelabel Error Page
There was an unexpected error (type=Internal Server Error, status=500).
To fix the error, we compare the returned string with the file name, which must be src/main/resources/templates/missing-page.html, with the same spelling and case. On Windows, Hello.html instead of hello.html works in the IDE but fails inside the JAR file, because paths in a JAR are case-sensitive. We can also replace the Whitelabel page with our own custom error pages, such as the templates error/404.html and error/500.html.
3.3. What Causes the Circular View Path Error?
When Thymeleaf is not on the classpath, Spring Boot falls back to the default InternalResourceViewResolver, which forwards the request to the URL /hello. But /hello is the controller itself, so the request would loop forever, and Spring MVC stops it with an error. The “Circular view path” error means Spring MVC has no template engine for the view name. For example, an app that starts without the Thymeleaf auto-configuration logs the error for GET /hello.
jakarta.servlet.ServletException: Circular view path [hello]: would dispatch back to the current handler URL [/hello] again. Check your ViewResolver setup! (Hint: This may be the result of an unspecified view, due to default view name generation.)
The fix depends on what the method should return.
| Cause | Fix |
|---|---|
| spring-boot-starter-thymeleaf is missing from pom.xml | Add the starter and reload the Maven project |
| The method should return data, not a page | Use @RestController, or add @ResponseBody to the method |
3.4. Should We Use spring-boot-starter-web or spring-boot-starter-webmvc?
For Spring Boot 4 projects, we use spring-boot-starter-webmvc. Spring Boot 4 split its auto-configuration into smaller modules, so the starter names now match the Spring Framework modules, e.g. spring-boot-starter-webmvc is for Spring MVC and spring-boot-starter-webflux is for WebFlux. In 4.1.1, spring-boot-starter-web brings the same libraries but is deprecated, so a project upgraded from Spring Boot 3 still compiles without changes. New projects use the new name, whereas Spring Boot 3.x has only spring-boot-starter-web.
3.5. Can We Use Spring MVC Without Spring Boot?
Yes. Spring MVC is part of the Spring Framework and does not need Spring Boot, but without Spring Boot we do the setup ourselves.
- Add @EnableWebMvc to a @Configuration class.
- Register the DispatcherServlet with a WebApplicationInitializer (or web.xml).
- Declare the Thymeleaf template resolver, template engine and view resolver as beans.
- Deploy a WAR file to an external Tomcat.
We write that configuration in a class that implements WebMvcConfigurer, whereas Spring Boot creates the same beans for us, which is the main difference between Spring and Spring Boot. For new applications, Spring Boot is the usual choice, because the same hello world needs one class less and no server installation.
4. Conclusion
A Spring MVC hello world application with Spring Boot 4 needs the spring-boot-starter-webmvc and spring-boot-starter-thymeleaf starters, a @Controller that fills the Model and returns a view name, and a template with the same name in src/main/resources/templates. The DispatcherServlet connects these parts for every request. For JSON responses, we switch to @RestController, and @WebMvcTest with MockMvc tests the controller without a server.
When something goes wrong, the status code tells us where to look. A 404 points to the URL mapping, and a 500 with TemplateInputException points to the template name. “Circular view path” is different. It means the template engine is missing.
5. References
- Spring Framework Reference: Spring Web MVC
- Spring Framework Reference: DispatcherServlet
- Spring Boot Reference: Servlet Web Applications
- Spring Boot Reference: Testing Spring Boot Applications
- Spring Framework Reference: MockMvc
- Thymeleaf 3.1: Using Thymeleaf
Happy Learning !!
How does Spring know where to find your Bean config file (spring-servlet.xml)? Other examples that I’ve seen specifies that location of that file with with in web.xml, or by AnnotationConfigWebApplicationContext class using pure Java configuration.
Ideally you should use contextConfigLocation as below.
spring
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
/WEB-INF/default-servlet.xml
1
If you do not specify, then spring will automatically look for +”-“+servlet.xml in the WEB-INF folder.
I really struggled very hard to get this :
It would be more useful if you specify the development environment.
I had to change the spring-servlet.xml as below to get the output.
Thanks for the feedback. Added in my TODO list.
What did you change in spring-servlet.xml ? Can you put a note on that ? I’m continue to get
“The origin server did not find a current representation for the target resource or is not willing to disclose that one exists.”
I meet same problem
I tried the above example in my own way. in that I did not know how to configure the xml file for above program it shows the error repeatedly(File not found like).
Will you please explain how to configure and what are the files need to be configured
The corresponding table in the jsp page is arriving but not displaying any data.
there is no data displayed in the .jsp because the list was not set to page/request/session/application attribute.
you need to set it in the controller
ex: request.setAttribute(“employees”, employees); or session.setAttribute(“employees”, employees);
hey i am start to use spring mvc 5.0.9 and use java based configuration , when try to access my jsp page
i get the folowing error message
Type Status Report
Message /Contactapp/WEB-INF/view/ hello.jsp
Description The origin server did not find a current representation for the target resource or is not willing to disclose that one exists.
Apache Tomcat/9.0.8
Did you checked the file name and without .jsp
the first one is to show different parts’ name,like employee or tourist
then url will be “http://localhost:8080/projectName/tourist/
the second one is to show one part’s method name. like login or logout
then url will be “http://localhost:8080/projectName/employee/login
hope to fix your problem.
Hi Lokesh,
your articles are simple to read and understand.
I have a question – what is the need of EmployeeManager and EmployeeManagerImpl classes here
Why you call ‘manager.getAllEmployees()’, instead we can call directly ‘dao.getAllEmployees()’ by using EmployeeDAO class.
Please clarify us.
Read my comments on this question : Service layer vs DAO — Why both?
How view page(jsp) gets to know about model attribute “employees” and gets iterated.?
Are we can manually configure all the request maping to handlermapping ??? Can you please elaborate this
@ModelAttributebinds a method parameter or method return value to a named model attribute, exposed to a web view. It is essentially form backing object. After controller method execution, it’s data is copied to to bothHttpServletRequestandHttpSessionand thus accessible to JSP.why thr is two @requestmapping there at class level and method level?
The class level maps to a url, and the second within the class maps to an even further extension. So class level starts it, and you can keep digging further into different pages.
So class level takes you too localhost:8080/employee-module
The method within the class takes you to localhost8080/employee-module/getAllEmployees
Hi – In the above example, can you pls add exception handling using @ExceptionHandler or @ControllerAdvice and illustrate how to handle exceptions thrown from DAO layer, via a common exception handling class in the controller layer?