Web Server #

Behind every web application — from online stores to monitoring dashboards — there’s a process listening on a TCP port, accepting HTTP requests, processing them, and sending back responses. That’s what a web server does. In the Java ecosystem, there are three main approaches: the Servlet API, which has been the foundational standard for two decades, Spring Boot, the dominant choice for modern enterprise applications, and Jetty, which can be embedded directly into a plain Java application. This article covers how each works — from a simple servlet to a complete REST API with routing, request parsing, response formatting, filters, and production configuration.

Overview #

Every HTTP request passes through layers before your application code runs:

flowchart LR
    A["Browser / API Client"] -->|"HTTP Request"| B["Web Server\n(Tomcat / Jetty / Netty)"]
    B --> C["Filter / Middleware\n(auth, logging, CORS)"]
    C --> D["Servlet / Controller\n(application logic)"]
    D --> E["Response\n(JSON / HTML / file)"]
    E --> A
ApproachGood forServerConfiguration
Servlet APILegacy enterprise apps, learning HTTP basicsTomcat, Jetty, WildFlyweb.xml or annotations
Spring BootModern apps, REST APIs, microservicesEmbedded Tomcat/JettyAuto-configuration
Embedded JettyInternal tools, lightweight standalone appsEmbedded JettyJava code

The Servlet API #

A servlet is a Java component that handles HTTP requests directly. This is the most basic layer — every Java web framework (including Spring) ultimately runs on top of the Servlet API. Understanding servlets means understanding how HTTP works in Java.

Dependencies #

<!-- Maven — Jakarta Servlet API (for Tomcat 10+) -->
<dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>6.0.0</version>
    <scope>provided</scope> <!-- provided by the server, not included in the WAR -->
</dependency>

Creating a Basic Servlet #

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

// @WebServlet registers the servlet at the /hello path without needing web.xml
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

    // doGet: handle HTTP GET requests
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {

        // Read a parameter from the query string: /hello?name=Budi
        String name = req.getParameter("name");
        if (name == null || name.isBlank()) name = "World";

        // Set the response content type
        res.setContentType("text/html; charset=UTF-8");
        res.setCharacterEncoding("UTF-8");

        // Write the response body
        PrintWriter out = res.getWriter();
        out.println("<!DOCTYPE html>");
        out.println("<html><body>");
        out.println("<h1>Hello, " + escapedHtml(name) + "!</h1>");
        out.println("</body></html>");
    }

    // doPost: handle HTTP POST requests
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {

        // Read form body from POST
        String name  = req.getParameter("name");
        String email = req.getParameter("email");

        // Set the response as JSON
        res.setContentType("application/json; charset=UTF-8");
        res.setStatus(HttpServletResponse.SC_CREATED); // 201

        res.getWriter().println("""
            {"status": "ok", "name": "%s", "email": "%s"}
            """.formatted(name, email));
    }

    private String escapedHtml(String input) {
        return input
            .replace("&", "&amp;")
            .replace("<", "&lt;")
            .replace(">", "&gt;");
    }
}

Reading Requests #

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException {

    // Path and method
    String method  = req.getMethod();       // GET, POST, PUT, etc.
    String path    = req.getRequestURI();   // /api/products/123
    String query   = req.getQueryString();  // id=123&sort=asc

    // Query string parameters
    String id   = req.getParameter("id");
    String[] tags = req.getParameterValues("tag"); // ?tag=a&tag=b → ["a", "b"]

    // Headers
    String contentType = req.getHeader("Content-Type");
    String userAgent   = req.getHeader("User-Agent");
    String bearer      = req.getHeader("Authorization"); // "Bearer <token>"

    // Client information
    String ip   = req.getRemoteAddr();
    String host = req.getServerName();
    int    port = req.getServerPort();

    // Path variable from the URL (with the pattern /products/*)
    String pathInfo = req.getPathInfo(); // "/123" for the URL /products/123

    // Session
    var session = req.getSession(false); // false = don't create one if it doesn't exist
    if (session != null) {
        String userId = (String) session.getAttribute("userId");
    }
}

Writing Responses #

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException {

    // Status code
    res.setStatus(200);                           // OK
    res.setStatus(HttpServletResponse.SC_OK);     // more readable
    res.sendError(404, "Product not found"); // set status + error body

    // Response headers
    res.setHeader("Cache-Control", "no-cache, no-store");
    res.setHeader("X-Request-Id", java.util.UUID.randomUUID().toString());
    res.addHeader("Set-Cookie", "sessionId=abc123; HttpOnly; Secure");

    // Redirect
    res.sendRedirect("/login");         // 302 redirect
    res.sendRedirect("https://example.com"); // redirect to another URL

    // JSON body
    res.setContentType("application/json; charset=UTF-8");
    res.getWriter().println("{\"id\": 1, \"name\": \"Laptop\"}");
}

Filters — Middleware for Servlets #

A filter is a component that runs before and after a servlet — suitable for logging, authentication, CORS, or compression.

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.*;
import java.io.IOException;
import java.time.Instant;

// Apply to all URLs
@WebFilter("/*")
public class LogFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest  httpReq = (HttpServletRequest)  req;
        HttpServletResponse httpRes = (HttpServletResponse) res;

        long start = System.currentTimeMillis();
        String path = httpReq.getRequestURI();

        // Add CORS headers
        httpRes.setHeader("Access-Control-Allow-Origin", "*");
        httpRes.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");

        // Handle preflight OPTIONS requests
        if ("OPTIONS".equalsIgnoreCase(httpReq.getMethod())) {
            httpRes.setStatus(HttpServletResponse.SC_OK);
            return;
        }

        // Continue to the next servlet
        chain.doFilter(req, res);

        // Log after the servlet finishes
        long duration = System.currentTimeMillis() - start;
        System.out.printf("[%s] %s %s → %d (%dms)%n",
            Instant.now(), httpReq.getMethod(), path, httpRes.getStatus(), duration);
    }
}

Running on Tomcat #

# Package into a WAR
mvn package -Dpackaging=war

# Deploy to Tomcat (copy to webapps/)
cp target/app.war $TOMCAT_HOME/webapps/

# Access at: http://localhost:8080/app/hello

Spring Boot #

Spring Boot is the most productive way to build a web server in Java today. It embeds Tomcat automatically, configures many things with convention over configuration, and provides a complete ecosystem — from security to databases. You don’t need an external server: just run main() and the server is up.

Dependencies and Setup #

<!-- Maven -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
// Gradle
implementation 'org.springframework.boot:spring-boot-starter-web'
// Application entry point
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication // = @Configuration + @EnableAutoConfiguration + @ComponentScan
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
        // Tomcat runs on port 8080
    }
}

REST Controller #

import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import java.util.*;

@RestController               // = @Controller + @ResponseBody
@RequestMapping("/api/products") // all endpoints start with /api/products
public class ProductController {

    // Simulated data (in a real app, this comes from a database)
    private final Map<Long, Product> db = new java.util.concurrent.ConcurrentHashMap<>();
    private long idCounter = 1;

    // GET /api/products — list all products
    @GetMapping
    public List<Product> getAllProducts(
            @RequestParam(required = false) String search,        // optional query param
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {

        return db.values().stream()
            .filter(p -> search == null || p.name().contains(search))
            .skip((long) page * size)
            .limit(size)
            .toList();
    }

    // GET /api/products/42 — one product by ID
    @GetMapping("/{id}")
    public ResponseEntity<Product> getProduct(@PathVariable Long id) {
        Product p = db.get(id);
        if (p == null) {
            return ResponseEntity.notFound().build(); // 404
        }
        return ResponseEntity.ok(p); // 200 + JSON body
    }

    // POST /api/products — create a new product
    @PostMapping
    public ResponseEntity<Product> createProduct(@RequestBody ProductRequest req) {
        // Manual validation (in production, use @Valid + Bean Validation)
        if (req.name() == null || req.name().isBlank()) {
            return ResponseEntity.badRequest().build(); // 400
        }

        Product created = new Product(idCounter++, req.name(), req.price());
        db.put(created.id(), created);

        // 201 Created + Location header
        return ResponseEntity
            .created(java.net.URI.create("/api/products/" + created.id()))
            .body(created);
    }

    // PUT /api/products/42 — update a product
    @PutMapping("/{id}")
    public ResponseEntity<Product> updateProduct(
            @PathVariable Long id,
            @RequestBody ProductRequest req) {

        if (!db.containsKey(id)) return ResponseEntity.notFound().build();

        Product updated = new Product(id, req.name(), req.price());
        db.put(id, updated);
        return ResponseEntity.ok(updated);
    }

    // DELETE /api/products/42 — delete a product
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
        if (db.remove(id) == null) return ResponseEntity.notFound().build();
        return ResponseEntity.noContent().build(); // 204
    }
}

// Records for the model (Java 16+)
record Product(Long id, String name, double price) {}
record ProductRequest(String name, double price) {}

Global Exception Handling #

Instead of handling exceptions in every controller, use @RestControllerAdvice to centralize error handling.

import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.WebRequest;

@RestControllerAdvice
public class GlobalExceptionHandler {

    // Handle a custom exception
    @ExceptionHandler(ProductNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(
            ProductNotFoundException ex, WebRequest request) {

        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            "NOT_FOUND",
            ex.getMessage(),
            request.getDescription(false)
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    // Handle all unexpected exceptions
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneric(Exception ex, WebRequest request) {
        ErrorResponse error = new ErrorResponse(
            500, "INTERNAL_ERROR", "An internal error occurred", request.getDescription(false)
        );
        return ResponseEntity.status(500).body(error);
    }
}

record ErrorResponse(int status, String code, String message, String path) {}
class ProductNotFoundException extends RuntimeException {
    public ProductNotFoundException(Long id) {
        super("Product with ID " + id + " not found");
    }
}

application.properties Configuration #

# Server port (default 8080)
server.port=8080

# Context path — all URLs start with /api
server.servlet.context-path=/

# Request timeout
server.connection-timeout=5s

# Maximum request body size (default 1MB)
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB

# Logging level
logging.level.root=INFO
logging.level.com.example=DEBUG

# Jackson — JSON format
spring.jackson.serialization.indent-output=true
spring.jackson.default-property-inclusion=NON_NULL

# Enable actuator endpoints for health checks
management.endpoints.web.exposure.include=health,info,metrics

Filters in Spring Boot #

import jakarta.servlet.*;
import jakarta.servlet.http.*;
import org.springframework.stereotype.Component;
import java.io.IOException;

@Component // Spring automatically registers this as a filter
public class RequestLoggingFilter implements jakarta.servlet.Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest  httpReq = (HttpServletRequest)  req;
        HttpServletResponse httpRes = (HttpServletResponse) res;

        long start = System.currentTimeMillis();
        chain.doFilter(req, res);
        long duration = System.currentTimeMillis() - start;

        System.out.printf("%s %s → %d (%dms)%n",
            httpReq.getMethod(), httpReq.getRequestURI(), httpRes.getStatus(), duration);
    }
}

Running Spring Boot #

# Via Maven
mvn spring-boot:run

# Via JAR (after building)
mvn clean package
java -jar target/app-1.0.0.jar

# With a specific profile
java -jar app.jar --spring.profiles.active=production

# Access: http://localhost:8080/api/products

Embedded Jetty #

Jetty lets you build a web server that runs entirely from Java code — no Tomcat, no WAR, no deployment to an external server. The entire server lives in a single JAR that can be run with java -jar. This is ideal for lightweight microservices, internal tools, or when you want full control over server configuration.

Dependencies #

<!-- Maven — Jetty 12 -->
<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-server</artifactId>
    <version>12.0.7</version>
</dependency>
<dependency>
    <groupId>org.eclipse.jetty.ee10</groupId>
    <artifactId>jetty-ee10-servlet</artifactId>
    <version>12.0.7</version>
</dependency>

Minimal Server #

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.ee10.servlet.*;
import jakarta.servlet.http.*;
import java.io.IOException;

public class JettyMain {
    public static void main(String[] args) throws Exception {
        // Create a server on port 8080
        Server server = new Server(8080);

        // Context handler: handle all requests at the "/" path
        ServletContextHandler ctx = new ServletContextHandler();
        ctx.setContextPath("/");

        // Register servlets at paths
        ctx.addServlet(new ServletHolder(new HelloServlet()), "/hello");
        ctx.addServlet(new ServletHolder(new ApiServlet()),   "/api/*");

        server.setHandler(ctx);

        // Start the server
        server.start();
        System.out.println("Jetty server running at http://localhost:8080");

        // Block until the server is stopped (Ctrl+C)
        server.join();
    }
}

// A simple servlet
class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res)
            throws IOException {
        res.setContentType("application/json");
        res.getWriter().println("{\"message\": \"Hello from Jetty!\"}");
    }
}

Thread Pool and Timeout Configuration #

import org.eclipse.jetty.server.*;
import org.eclipse.jetty.util.thread.QueuedThreadPool;

public class ConfiguredJetty {
    public static void main(String[] args) throws Exception {
        // Thread pool configuration
        QueuedThreadPool pool = new QueuedThreadPool();
        pool.setMinThreads(5);
        pool.setMaxThreads(50);
        pool.setIdleTimeout(60_000); // remove idle threads after 60 seconds

        Server server = new Server(pool);

        // Connector configuration (port, timeouts)
        ServerConnector connector = new ServerConnector(server);
        connector.setPort(8080);
        connector.setIdleTimeout(30_000); // drop idle connections after 30 seconds
        connector.setAcceptQueueSize(100); // incoming connection queue

        server.addConnector(connector);

        // ... add handlers and start
        server.start();
        server.join();
    }
}

Custom Handlers (Without Servlets) #

For very simple cases, Jetty also supports Handler directly without the Servlet API:

import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.AbstractHandler;
import jakarta.servlet.http.*;
import java.io.IOException;

public class CustomHandler extends AbstractHandler {

    @Override
    public void handle(String target, Request baseReq,
                        HttpServletRequest req, HttpServletResponse res)
            throws IOException {

        res.setContentType("application/json; charset=utf-8");
        res.setStatus(HttpServletResponse.SC_OK);

        String response = switch (target) {
            case "/ping"   -> "{\"status\": \"ok\"}";
            case "/version"  -> "{\"version\": \"1.0.0\"}";
            default        -> "{\"error\": \"endpoint not found\"}";
        };

        res.getWriter().println(response);
        baseReq.setHandled(true); // mark as handled
    }
}

Direct Comparison #

To build a GET /api/products endpoint returning JSON, here’s the comparison of the three approaches:

Servlet API #

@WebServlet("/api/products")
public class ProductServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res)
            throws IOException {
        res.setContentType("application/json");
        res.getWriter().println("[{\"id\":1,\"name\":\"Laptop\"}]");
    }
}

Spring Boot #

@RestController
@RequestMapping("/api")
public class ProductController {
    @GetMapping("/products")
    public List<Map<String, Object>> products() {
        return List.of(Map.of("id", 1, "name", "Laptop"));
    }
}

Embedded Jetty #

ctx.addServlet(new ServletHolder(new HttpServlet() {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res)
            throws IOException {
        res.setContentType("application/json");
        res.getWriter().println("[{\"id\":1,\"name\":\"Laptop\"}]");
    }
}), "/api/products");

Spring Boot is the most concise because it handles JSON serialization automatically. Servlets and Jetty require manual serialization or an additional library like Jackson.


When to Use Each Approach #

Use the SERVLET API when:
  ✓ Learning HTTP basics and how Java web servers work
  ✓ You need to deploy to an existing app server (Tomcat, JBoss, WebLogic)
  ✓ Maintaining legacy applications that already use Servlets
  ✗ Avoid it for new applications — too verbose compared to the alternatives

Use SPRING BOOT when:
  ✓ Building new REST APIs or microservices
  ✓ You need a complete ecosystem: security, database, caching, testing
  ✓ The team is already familiar with Spring
  ✓ Auto-configuration saves setup time
  ✓ Almost always — it's the default choice for modern Java web development

Use EMBEDDED JETTY when:
  ✓ You need a lightweight server embedded in a standalone application
  ✓ Internal tools or desktop apps with a small web feature
  ✓ Full control over server configuration without Spring's overhead
  ✓ Distribution as a single JAR without external dependencies

Production tips for all approaches:
  ✓ Always set timeouts for requests and connections
  ✓ Add a logging filter for observability
  ✓ Handle exceptions centrally — don't let stack traces leak to clients
  ✓ Set security headers: Content-Security-Policy, X-Frame-Options, etc.
  ✓ Use HTTPS in production — configure SSL/TLS on the server or reverse proxy

Summary #

  • The Servlet API is the foundation — every Java web framework runs on top of it. HttpServlet, doGet(), doPost(), HttpServletRequest, and HttpServletResponse are building blocks you need to understand.
  • Spring Boot is the default choice for new applications@RestController + @GetMapping/@PostMapping + @RequestBody + ResponseEntity form a clean, idiomatic REST API pattern.
  • ResponseEntity for full response control — you can set the status code, headers, and body explicitly. Use ResponseEntity.ok(), .created(), .notFound(), .badRequest().
  • @RestControllerAdvice for centralized error handling — instead of try-catch in every controller, catch specific exceptions in one place and return a consistent error format.
  • Filters for cross-cutting concerns — logging, authentication, CORS, and compression are filter concerns, not controller concerns. Keep them separate so controllers stay focused on business logic.
  • Embedded Jetty for single-JAR distribution — good for tools and microservices that need to run anywhere without a server installation.
  • Set timeouts — without timeouts, one slow request can block the thread pool and kill the server. Set server.connection-timeout in Spring Boot or setIdleTimeout in Jetty.
  • Don’t leak error details to clients — stack traces contain sensitive information. Catch all exceptions and return safe, informative error messages.

← Previous: Web Socket   Next: Unit Test →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact