Java Web & Servlets Fundamentals

Setting Up a Java Web Project

18 min Lesson 2 of 13

Setting Up a Java Web Project

Before you write a single servlet you need to understand the standardised on-disk layout that every Jakarta EE web container expects, how to bundle your application into a WAR file, and how to get that WAR running inside Apache Tomcat. This lesson is entirely practical: by the end you will have a project structure you can reuse for every servlet you write in this tutorial.

The Standard Web Application Directory Layout

A Jakarta EE web application follows a precise directory contract defined by the Servlet specification. Every compliant container — Tomcat, Jetty, WildFly — honours the same layout:

my-webapp/ ├── src/ │ └── main/ │ ├── java/ <-- your servlet and helper classes │ │ └── com/example/web/ │ └── webapp/ <-- the web root (maps to the URL root) │ ├── index.html │ ├── css/ │ ├── js/ │ └── WEB-INF/ <-- protected, never served directly │ ├── web.xml <-- deployment descriptor (optional but common) │ ├── classes/ <-- compiled .class files (built automatically) │ └── lib/ <-- dependency JARs bundled with the app └── pom.xml <-- Maven build file

The most important rule to memorise: everything under WEB-INF/ is invisible to the browser. A request for /WEB-INF/web.xml returns a 404 from the container, not the file. This is how JSPs that must not be accessed directly, sensitive config, and compiled classes are kept safe. Anything placed outside WEB-INF/ — HTML, images, CSS — is publicly accessible.

WEB-INF is not a security boundary — it is a visibility boundary. The container enforces it, but you should still never put passwords or keys in web.xml. Use environment variables or a secrets manager for credentials, exactly as you would in any other server-side application.

The Deployment Descriptor: web.xml

The file WEB-INF/web.xml is the deployment descriptor. It is an XML file that tells the container about your application: which servlets exist, which URL patterns map to them, session timeout, welcome files, error pages, and security constraints. Since Servlet 3.0, annotations replace most of its content for simple cases, but web.xml remains important for settings that annotations cannot express and for environments where annotation scanning is disabled.

A minimal but complete web.xml for Jakarta EE 10 (Servlet 6.0) looks like this:

<?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"> <display-name>My First Web App</display-name> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list> <session-config> <session-timeout>30</session-timeout> <!-- minutes --> </session-config> </web-app>

No servlet mappings are declared here because the next lesson introduces the @WebServlet annotation, which is the modern way to bind a URL to a servlet class without touching web.xml.

Keep web.xml even when using annotations. An empty-but-valid web.xml lets you override annotation-based settings per-environment (e.g., a different session timeout in production) without recompiling. It also serves as explicit documentation of the application's contract.

Setting Up the Maven Build (pom.xml)

Maven's war packaging type knows about the web application layout and produces a correctly structured WAR automatically. Here is a minimal pom.xml that targets Jakarta EE 10 / Tomcat 10.1:

<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>com.example</groupId> <artifactId>my-webapp</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- Servlet API — provided by Tomcat at runtime, not bundled --> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>6.0.0</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>my-webapp</finalName> </build> </project>

The provided scope on the servlet API dependency is critical. It means Maven compiles against the API but does not bundle it into the WAR. Tomcat already ships the API; if you bundled it too you would get classloader conflicts and hard-to-debug ClassCastException errors at runtime.

Tomcat 10+ uses jakarta.*, not javax.*. If your imports still read import javax.servlet.* you are targeting the older Java EE namespace and your code will not deploy on Tomcat 10 or any Jakarta EE 10 container. The rename happened with the Jakarta EE 9 specification in 2020. Always check your Tomcat version against the servlet-api version you depend on.

Building the WAR

A WAR (Web Application Archive) is simply a ZIP file with a .war extension and the directory layout described above baked in. To build it:

mvn clean package

Maven compiles your Java sources, copies them to WEB-INF/classes/, copies dependency JARs (those not scoped provided) to WEB-INF/lib/, includes everything under src/main/webapp/, and produces target/my-webapp.war. You can verify the contents:

jar tf target/my-webapp.war

Expected output includes entries like WEB-INF/web.xml, WEB-INF/classes/com/example/web/HelloServlet.class, and any static assets.

Deploying to Tomcat

Tomcat 10.1 ships with a webapps/ directory. Drop a WAR there and Tomcat auto-deploys it:

# Copy the WAR to Tomcat's webapps directory cp target/my-webapp.war $CATALINA_HOME/webapps/ # Start Tomcat (if not already running) $CATALINA_HOME/bin/startup.sh # Linux / macOS %CATALINA_HOME%\bin\startup.bat # Windows

Tomcat extracts the WAR into a matching directory (e.g., webapps/my-webapp/) and the application becomes available at http://localhost:8080/my-webapp/. The context path (/my-webapp) is derived from the WAR filename. To deploy to the root context, rename the WAR to ROOT.war.

For development, the Tomcat Maven Plugin lets you hot-deploy without manual copying:

<plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat10-maven-plugin</artifactId> <version>2.0-beta-1</version> <configuration> <port>8080</port> <path>/my-webapp</path> </configuration> </plugin>
mvn tomcat10:run

This starts an embedded Tomcat instance, deploys your application, and keeps it running. You get a fast edit-compile-refresh cycle without touching a standalone Tomcat installation.

Checking the Logs

Tomcat writes startup and deployment output to $CATALINA_HOME/logs/catalina.out. If your WAR fails to deploy — missing web.xml namespace, a bad servlet mapping, a classloader conflict — the error message is here. Reading this log is the first step whenever a deployment goes wrong.

Summary

You now have a reusable mental model: static content lives under the webapp root; everything sensitive lives under WEB-INF/; web.xml describes the application contract; provided-scoped servlet API avoids classloader conflicts; mvn clean package produces the WAR; dropping it in webapps/ deploys it. In the next lesson you will write the first real servlet and map it to a URL.