Your First Servlet Application
Introduction
This chapter builds a minimal Jakarta Servlet app with Maven, packages it as a WAR, deploys it to standalone Tomcat, and verifies http://localhost:8080/hello/. You will use @WebServlet (no heavy web.xml), provided scope for the Servlet API, and the same directory conventions as Your First Maven Project—extended with src/main/webapp for web assets.
Prerequisites
- Installing Tomcat — Tomcat 10.1.x running
- Deploying WAR and webapps — context path rules
- Maven and JDK 17+ (
mvn -version) - Optional: Maven dependency scopes —
provided
Goal
Maven project → mvn package → target/hello.war → webapps/ → curl /hello/api| Step | Output |
|---|---|
| Build | target/hello.war |
| Deploy | Context path /hello |
| Servlet URL | GET http://localhost:8080/hello/api → plain text response |
| Static page | GET http://localhost:8080/hello/ → index.html |
Create the Maven WAR Project
From your projects folder:
mkdir -p hello/src/main/java/com/example
mkdir -p hello/src/main/webapp/WEB-INF
cd helloCreate pom.xml at the project root:
Code explanation:
<packaging>war</packaging>— Maven produces a WAR, not a JARjakarta.servlet-apiwithprovided— compile against the API; Tomcat supplies implementation JARs at runtime<finalName>hello</finalName>— outputtarget/hello.war(matches context path/hello)
Warning
Use jakarta, Not javax
Tomcat 10 requires jakarta.servlet.*. javax.servlet is for Tomcat 9 and older stacks.
Directory Layout vs JAR Project
Compare to a typical Maven JAR:
hello/ my-first-app/ (JAR)
├── pom.xml ├── pom.xml
└── src/ └── src/
└── main/ └── main/
├── java/ ├── java/ ← same: Java sources
│ └── com/example/ │ └── com/example/
│ └── HelloServlet.java │ └── App.java
└── webapp/ └── resources/ ← JAR: config only
├── index.html
└── WEB-INF/
└── web.xml (optional)| Path | Role |
|---|---|
src/main/java | Servlets, filters, plain Java classes |
src/main/webapp | Document root of the WAR—becomes public URLs under context path |
src/main/webapp/WEB-INF | Private web metadata and non-public resources |
src/main/resources | Optional classpath resources (.properties in WEB-INF/classes after build) |
Maven mvn package compiles Java into WEB-INF/classes/ inside the WAR and copies webapp/ contents to the WAR root.
Write HelloServlet
src/main/java/com/example/HelloServlet.java:
Code explanation:
@WebServlet("/api")— path inside the app; full URL is/hello/apidoGet— handles HTTP GET; usedoPostfor POSTHttpServlet— Tomcat callsservice(), which dispatches by HTTP method
Add a Static Welcome Page
src/main/webapp/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello WAR</title>
</head>
<body>
<h1>hello_code Tomcat WAR</h1>
<p>Try the API: <a href="api">/hello/api</a></p>
</body>
</html>Optional minimal src/main/webapp/WEB-INF/web.xml (welcome file only):
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0">
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>Code explanation:
index.htmlat WAR root →http://localhost:8080/hello/or/hello/index.html- Servlet API 3+ finds
@WebServleteven withoutweb.xml—the welcome file XML is optional clarity
Build the WAR
cd hello
mvn clean packageExpected: BUILD SUCCESS and target/hello.war.
Inspect contents:
jar tf target/hello.warYou should see:
index.html
WEB-INF/web.xml
WEB-INF/classes/com/example/HelloServlet.class
WEB-INF/lib/ ← empty unless you add dependencies
META-INF/MANIFEST.MFDeploy to Tomcat
Stop Tomcat if replacing an older deploy:
export CATALINA_HOME=/opt/tomcat # your path
$CATALINA_HOME/bin/shutdown.shCopy WAR:
cp target/hello.war $CATALINA_HOME/webapps/
$CATALINA_HOME/bin/startup.shWait for deploy (watch logs):
tail -f $CATALINA_HOME/logs/catalina.$(date +%Y-%m-%d).logLook for a line indicating deployment of context /hello.
Verify
curl http://127.0.0.1:8080/hello/api
curl -I http://127.0.0.1:8080/hello/Expected:
Hello from Tomcat Servlet
HTTP/1.1 200Browser:
http://127.0.0.1:8080/hello/
http://127.0.0.1:8080/hello/apiTip
404 on /api but Not /hello/api
Servlet mapping is relative to the context. Use /hello/api, not /api, when context path is /hello.
Redeploy After Code Changes
Development loop:
# 1. Edit HelloServlet.java
mvn clean package
$CATALINA_HOME/bin/shutdown.sh
rm -rf $CATALINA_HOME/webapps/hello $CATALINA_HOME/webapps/hello.war
cp target/hello.war $CATALINA_HOME/webapps/
$CATALINA_HOME/bin/startup.sh
curl http://127.0.0.1:8080/hello/apiFor faster iteration, use catalina.sh run in one terminal and Manager upload—or later Spring Boot embedded server. Standalone Tomcat favors rebuild + redeploy for correctness.
Run from IntelliJ IDEA (Optional)
- Open the
pom.xmlas a Maven project - Confirm Project SDK is JDK 17+
- Enable Maven auto-import
- Run → Edit Configurations → + → Tomcat Server → Local (Ultimate) or deploy WAR manually on Community
Community Edition: build with mvn package, copy WAR to webapps/—no bundled Tomcat runner required.
Common Build and Deploy Errors
| Error | Cause | Fix |
|---|---|---|
package javax.servlet does not exist | Wrong API | Use jakarta.servlet for Tomcat 10 |
BUILD FAILURE compiler | JDK too old | maven.compiler.release 17+ |
404 on /hello/api | Deploy not finished; wrong URL | Check logs; use full path with context |
| 500 on first hit | Servlet exception | Read logs/localhost.*.log stack trace |
WAR deploys as /hello-1.0-SNAPSHOT | Missing finalName | Set <finalName>hello</finalName> or rename WAR |
ClassNotFoundException for servlet | WAR missing classes | mvn clean package; verify WEB-INF/classes in WAR |
How This Differs from Spring Boot
| Topic | This chapter (Servlet + Tomcat) | Spring Boot |
|---|---|---|
| Entry | @WebServlet, web.xml | @RestController, @SpringBootApplication |
| Run locally | Deploy WAR to Tomcat | mvn spring-boot:run |
| Servlet API | You call HttpServletResponse | Spring MVC abstracts request/response |
| Config | web.xml, context.xml | application.yml |
Spring Boot still uses Tomcat embedded by default—you are learning the container-native path that WAR deploy and legacy apps use.
Post-Chapter Checklist
-
packagingiswarwithjakarta.servlet-apiprovided -
mvn packageproducestarget/hello.war - WAR deployed;
curlreturns greeting from/hello/api -
index.htmlserved at/hello/ - You can explain URL = context path + servlet path
What Comes Next
| Chapter | Topic |
|---|---|
| 09 — Servlet, JSP, and Filter basics | Lifecycle, POST, sessions, filters |
| 16 — Project: Maven WAR deploy | Extended exercise |
| 14 — Spring Boot WAR on Tomcat | Boot app on external Tomcat |
FAQ
Can I use mvn archetype for WAR?
Yes:
mvn archetype:generate \
-DgroupId=com.example \
-DartifactId=hello \
-DarchetypeArtifactId=maven-archetype-webapp \
-DarchetypeVersion=1.5 \
-DinteractiveMode=falseThen replace generated index.jsp with this chapter’s Servlet and add jakarta.servlet-api provided.
Why provided scope for servlet-api?
Tomcat already includes the Servlet implementation. Bundling it in WEB-INF/lib can cause class loader conflicts.
Deploy as ROOT (no /hello prefix)?
Rename artifact to ROOT.war or use conf/Catalina/localhost/ROOT.xml—see Deploying WAR.
Test without Tomcat?
Use embedded runners (IDE Tomcat, Spring Boot) or maven-surefire with HttpServlet unit tests and mocks—out of scope here; production path is Tomcat deploy.
Java version 17 vs 21?
Both work on Tomcat 10.1 with matching maven.compiler.release. Match your team JDK.
Where is web.xml generated?
Only if you create it under src/main/webapp/WEB-INF/. Annotations-only apps may omit it entirely.