Table of Contents
- Introduction
- Modules
- Common
- Installation
- Guide
- Header Models
- Header Data Structures
- Enums
- URL Building
- I/O Utilities
- Other Utilities
- Server
- Installation
- Guide
JetServer.BuilderConfigurationHandle- Exceptions
- Useful
HandlerImplementations- Directory
- Redirect
RouterImplementationsRouteImplementationsSession&SessionStoreImplementations- Let's Encrypt SSL Certificates
- OpenAPI Annotations
- Installation
- Guide
- OpenAPI Annotations Plugin
- Installation
- Guide
- Example Configuration (
build.gradle.kts) - Why not an annotation processor?
- Example Configuration (
- Client
- Common
- Personal Note About AI
- Alternatives
Introduction
Jet is a simple, lightweight, modern, turnkey, Java web client and server library.
Jet is a wrapper around the excellent Jetty web client and server library. Jetty provides the battle-tested low-level protocol handling, while Jet focuses on providing a modern and consistent interface with superb documentation and an amazing developer experience.
Jet offers four modules: Common, Server, OpenAPI Annotations, OpenAPI Annotations Plugin, and Client.
A few more awesome things about Jet:
- Exhaustive Javadoc documentation (all public classes, fields, and methods have Javadocs)
- Amazing developer experience
- High quality modern Java code (no AI slop)
- No runtime annotation magic
- A library, not a framework (structure your codebase however you prefer)
- Lightweight with minimal dependencies
- Wide code coverage via unit testing
- Releases are built directly from this source repository using GitHub Actions CI
- Kotlin friendly
- MIT licensed
Give this repository a star ⭐ and consider sponsoring ❤️
Modules
Common
The common module for various Jet modules.
This module contains many useful model classes and utilities for all your web server and client needs. For example,
instead of crafting a response cookie header value using manual string concatenation, you can use
Cookie.builder().name("name").value("value").httpOnly().secure().build().toResponseString(). For another example,
instead of crafting a URL string using manual string concatenation, you can use
Url.builder().scheme(HTTPS).host("example.com").addQueryParameter("key", "value").build().toString(). There are many
header models supported, such as Content-Security-Policy, ETag, ContentEncoding. There are many enums supported,
such as Method (e.g. GET, POST), Status (e.g. 404 Not Found), Version (e.g. HTTP/2), and more. All of these
classes exist in an effort to improve developer experience, increase type-safety and single-source-of-truth (e.g. no
scattered variables or duplicate string constants), and decrease bugs. See the guide for all models classes
and utilities this module provides.
Installation
This module is transitively depended on by the Server and Client modules, so you typically don't need to install this module directly.
For build.gradle.kts:
dependencies {
implementation("net.jacobpeterson.jet:common:3.3.0")
}For build.gradle:
dependencies {
implementation 'net.jacobpeterson.jet:common:3.3.0'
}For pom.xml:
<dependency> <groupId>net.jacobpeterson.jet</groupId> <artifactId>common</artifactId> <version>3.3.0</version> </dependency>
Guide
Most classes in this module are immutable and follow the builder pattern for creation.
Header Models
StrictTransportSecurityRangeIfRangeETagCookieContentTypeContentSecurityPolicyContentRangeContentEncodingContentDispositionRequestCacheControlResponseCacheControlBasicAuthenticationAcceptEncodingAccept
Header Data Structures
Case-insensitive key-value pairs:
Enums
URL Building
I/O Utilities
Other Utilities
A simple, lightweight, modern, turnkey, Java web server library.
Features:
- HTTP/1, HTTP/1.1, HTTP/2
- HTTPS encryption with SSL/TSL certificate hot-swap reloading
- Custom routing and handlers
- Sessions
- Resource serving (classpath files, filesystem files,
InputStream) - Server-Sent Events (SSE)
- WebSockets (coming soon)
- Multipart request body
- Response body compression (Zstandard, Brotli, Gzip, Deflate)
- Virtual threads (no more ugly async/reactive programming)
Installation
For build.gradle.kts:
dependencies {
implementation("net.jacobpeterson.jet:server:3.3.0")
}For build.gradle:
dependencies {
implementation 'net.jacobpeterson.jet:server:3.3.0'
}For pom.xml:
<dependency> <groupId>net.jacobpeterson.jet</groupId> <artifactId>server</artifactId> <version>3.3.0</version> </dependency>
Guide
Here's a very simple example to get you going:
static void main() { JetServer.builder() .sslLetsEncrypt() // Enable Let's Encrypt SSL/TLS .sessionStore() // Enable in-memory sessions .router(ImmutableSimpleRouter.builder() // Add a custom handler .addLast(PathExactRoute.builder().path("/custom-path").build(), handle -> handle.getResponse().responseHtml("<h1>Custom handler!</h1>")) // Serve files from `~/webroot/` .addLast(PathStartsWithRoute.builder().path("/").build(), FileDirectoryHandler .simpleMutable(Path.of(System.getProperty("user.home"), "webroot"), null, true)) .build()) .build(); // Automatically starts the server and adds a JVM shutdown hook to stop the server gracefully }
JetServer
represents the web server instance. Use the builder to configure and start the web server.
JetServer.Builder Configuration
handleFactory(HandleFactory)sessionStore()sessionStore(SessionStore)router(Router)preventMimeSniffing(boolean)preventAmbiguousResponseCacheControl(boolean)host(String)httpPort(int)httpsPort(int)http2(boolean)sslLetsEncrypt()sslDirectory(Path, Predicate, Predicate)sslPem(Path, Path)sslPem(String, String)sslPems(Supplier)reloadSslPeriod(Duration)gracefulStopTimeout(Duration)connectionIdleTimeout(Duration)connectionIdleTimeoutWhenStopping(Duration)
Handle
Handle
is a class that represents a web server request and response. It has getters for the
Request
and
Response
objects. Features: multipart request bodies, dynamic transparent response compression config,
Resource
serving,
SSE,
and more. For HTML templating, Jte is recommended as it provides the best features with the best
type-safety out of all major Java templating engine libraries.
The
Handler
functional interface is provided a
Handle
instance for the current HTTP request/response lifecycle. Using a functional interface makes it very easy to wrap/nest
Handler implementations, which is the mechanism for implementing middleware.
Exceptions
StatusException
is thrown by some methods for 400 Bad Request (for example if a request header is malformed) and is used as a silent
Exception for breaking the flow of execution in an implementation handler and returning a response status code.
BodyStreamException
is thrown by some methods for request/response body streaming client timeouts and early disconnects.
The Handler should not consume these exceptions. JetServer expects these exceptions to be thrown for special
processing (e.g. to set the response status upon a StatusException or to not log an error for BodyStreamException).
Useful Handler Implementations
Directory
Handlers for serving files from the classpath or filesystem with support for relativizing request paths, serving a
default file (like /index.html) for request paths representing a directory, serving a default file for requests paths
representing a file without an extension (like /index), redirecting to default files or default extensions, applying a
ResponseCacheControl,
and caching
Resource
instances from with automatic cache invalidation using
WatchService:
Redirect
Simple handlers for common redirection cases:
Router Implementations
These Router implementations represent a priority list for registered Route instances. Only one Route will be
matched for a request/response lifecycle.
Route Implementations
These Route implementations are added to the Router and tested for a match when a request is received.
Session & SessionStore Implementations
HTTP sessions store data server-side on behalf of a client. The client stores a cookie to be sent to the server to reference the stored server-side data. These simple implementations store data in memory and have sensible defaults to get you started with HTTP sessions:
A recommended, but more advanced, implementation is to use Redis/Valkey via Redisson with RLO for better type-safety and persisting session data after a server restart.
Let's Encrypt SSL Certificates
JetServer has native support for Let's Encrypt SSL certificates. Follow these steps to add
HTTPS support to your web server for free. This guide assumes you're using an Ubuntu Linux VM hosted on an AWS EC2
instance, but can be adapted to other Linux distributions.
-
Install Java Amazon Corretto 25 for an unprivileged user named
jet:- Via SDKMAN!:
sudo useradd -ms /bin/bash jet sudo apt install zip # https://sdkman.io curl -s "https://get.sdkman.io" | sudo -u jet bash sudo -iu jet bash -ic "sdk install java 25.0.3-amzn"
- Via Apt repositories:
sudo useradd -ms /bin/bash jet # https://docs.aws.amazon.com/corretto/latest/corretto-25-ug/generic-linux-install.html wget -O - https://apt.corretto.aws/corretto.key | \ sudo gpg --dearmor -o /usr/share/keyrings/corretto-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/corretto-keyring.gpg] https://apt.corretto.aws stable main" | \ sudo tee /etc/apt/sources.list.d/corretto.list sudo apt-get update sudo apt-get install -y java-25-amazon-corretto-jdk
- Via SDKMAN!:
-
Install
certbotand add a renewal hook that allows users in theletsencryptgroup to read certificate files:sudo snap install certbot --classic sudo groupadd letsencrypt sudo usermod -aG letsencrypt jet sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy/ sudo tee /etc/letsencrypt/renewal-hooks/deploy/change-permissions.sh <<EOF #!/bin/bash chgrp -R letsencrypt /etc/letsencrypt/{live,archive} chmod -R g+rX /etc/letsencrypt/{live,archive} EOF sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/change-permissions.sh
-
Update your DNS A (IPv4) and AAAA (IPv6) records to point to your server. Ensure your AWS EC2 Security Group allows ports
80and443from any IPv4 or any IPv6 address. Configurenftables(successor to theiptables) to redirect privileged ports80and443to the unprivileged default web server ports8080and8443:sudo tee -a /etc/nftables.conf <<EOF table inet nat { chain prerouting { type nat hook prerouting priority dstnat; policy accept; tcp dport { 80, 443 } redirect to tcp dport map { 80 : 8080, 443 : 8443 } } chain output { type nat hook output priority dstnat; policy accept; fib daddr type local tcp dport { 80, 443 } redirect to tcp dport map { 80 : 8080, 443 : 8443 } } } EOF sudo systemctl enable --now nftables
-
In your Maven or Gradle project with the Server module installed, use
FileDirectoryHandlerto serve a webroot for thecertbotcertificate renewal process and run your Maven or Gradle project as thejetuser on your server:sudo -iu jet bash -ic "mkdir webroot"JetServer.builder() .router(ImmutableSimpleRouter.builder() .addLast(PathStartsWithRoute.builder().path("/.well-known/acme-challenge/").build(), FileDirectoryHandler.simpleMutable(Path.of("webroot"), null, true)) .build()) .build();
-
Now request your SSL certificate:
sudo certbot certonly -d <your_domain> --webroot /home/jet/webroot/(specify/home/jet/webroot/as the webroot) -
Now configure
JetServer.Builderwith.sslLetsEncrypt():JetServer.builder() .sslLetsEncrypt() // Automatically enables hot-swap SSL reloading .router(ImmutableSimpleRouter.builder() // Keep the `FileDirectoryHandler` for future `certbot` auto-renewals. .addLast(PathStartsWithRoute.builder().path("/.well-known/acme-challenge/").build(), FileDirectoryHandler.simpleMutable(Path.of("webroot"), null, true)) .addLast(PathExactRoute.builder() .schemeEnum(HTTPS) .path("/") .build(), handle -> handle.getResponse().responseText("HTTPS yay!")) .build()) .build();
Because of Jet Server's native support for SSL/TLS, it is generally not recommended to put Jet Server behind a reverse proxy (such as Nginx) to handle SSL/TLS termination. Unless you have other web server requirements, like using PHP FPM via Nginx to support PHP applications, you should use Jet Server as the main web server on your Linux VM.
OpenAPI Annotations
A code-first OpenAPI specification annotations library.
This module provides Java annotations for all OpenAPI objects defined within the
OpenAPI Description. This allows you to
define OpenAPI specifications directly within Java code using Java annotations so that your API specification lives
alongside the API implementation handlers. This module is built in a way that enforces type-safety and encourages
single-source-of-truth within API specifications. For example, instead of declaring the path
"/api/account/profile-picture" separately in your OpenAPI spec, in the Java web server handler code, and in the client
API request code (e.g. a JavaScript fetch() call), you can declare the path once as a string constant in Java,
reference that same constant within an OpenAPI annotation, generate an OpenAPI spec from those annotations, and then
generate a client library from that OpenAPI spec. This way, everything is type-safe and dervied from a
single-source-of-truth, which reduces bugs caused by typos, forgetting to update request paths and JSON schemas, etc.
One of the best features this module offers is
OpenApiSchema.fromClass()
which enables you to include JSON schemas generated directly from Java classes in your OpenAPI specification, which is
the pinnacle of type-safety and single-source-of-truth! The enables you to create a strong client-server API contract in
which the Java server defines the contract that all generated OpenAPI clients adhere to. The excellent
victools/jsonschema-generator library is used to generate the JSON
schemas in the OpenAPI Annotations Plugin, so generics, inheritance, nullability, etc.
are all supported! Note that you can enforce @Nullable annotations on the server in implementation handlers using
NullableUtil.requireNonNullFieldsSet().
Installation
For build.gradle.kts:
dependencies {
implementation("net.jacobpeterson.jet:openapi-annotations:3.3.0")
}For build.gradle:
dependencies {
implementation 'net.jacobpeterson.jet:openapi-annotations:3.3.0'
}For pom.xml:
<dependency> <groupId>net.jacobpeterson.jet</groupId> <artifactId>openapi-annotations</artifactId> <version>3.3.0</version> </dependency>
Guide
All OpenAPI objects defined within the OpenAPI description have been implemented, enabling you to define an OpenAPI specification using Java annotations with no limitations:
OpenApiOpenApiCallbackOpenApiComponentsOpenApiContactOpenApiDiscriminatorOpenApiEncodingOpenApiExampleOpenApiExternalDocOpenApiHeaderOpenApiInfoOpenApiLicenseOpenApiLinkOpenApiMediaTypeOpenApiOAuthFlowOpenApiOAuthFlowsOpenApiOperationOpenApiParameterOpenApiPathItemOpenApiPathsOpenApiReferenceOpenApiRequestBodyOpenApiResponseOpenApiResponsesOpenApiSchemaOpenApiSecurityRequirementOpenApiSecuritySchemeOpenApiServerOpenApiServerVariableOpenApiTagOpenApiXml
There are few quirks when working with Java annotations. First, annotation methods/values cannot be set to null, so
some OpenAPI annotation methods have been marked with the special annotation
AnnotationArrayIsNullableValue
to denote that the array represents a nullable value, meaning null is defined as an empty array and non-null is
defined as an array with a single element. Second, to support maps/dictionaries, some OpenAPI annotation methods have
been marked with the special annotation
AnnotationArrayIsMap
to denote that the array represents a map with annotation entries that uses the special annotation
AnnotationArrayIsMapKey
to denote the map key.
Note that some OpenAPI annotations encourage the use of the models and enums from Common module. For example,
OpenApiPathItem.MethodEntry
has both
key
which can be set to a String constant, and
keyEnum
which can be set to a
Method
enum. However, an unfortunate limitation of Java annotations with methods of enum types is that they cannot be set to
constant references and must be set to the enum constant directly. This makes implementing the single-source-of-truth
practice a bit harder, since, for example, you cannot declare the constant
public static final Method METHOD = Method.GET and use that constant in a type-safe way within the annotation
declaration and for the web server implementation handler route registration. A workaround is to use String references
instead, which Java allows enum methods to reference. The Common module provides a ToString inner class for
all enums so they can be used within Java annotations as constant references. For example,
public static final Method METHOD = Method.GET becomes public static final String METHOD = Method.ToString.GET. Now
this string constant can be directly referenced both by the OpenAPI annotation and the web server implementation
handler route registration, so we keep our type-safety and single-source-of-truth practice!
Here is an example of the recommended approach for using OpenAPI annotations and implementation handlers with Jet using
type-safety and single-source-of-truth practices. Note that this example uses static inner classes for Web and
AccountHandlers, but ideally these would be separate class files, to better manage separation of concerns and to not
include so much code in one file.
import lombok.Builder; import lombok.Getter; import lombok.Value; import net.jacobpeterson.jet.openapiannotations.OpenApi; import net.jacobpeterson.jet.openapiannotations.OpenApiInfo; import net.jacobpeterson.jet.openapiannotations.OpenApiMediaType; import net.jacobpeterson.jet.openapiannotations.OpenApiOperation; import net.jacobpeterson.jet.openapiannotations.OpenApiParameter; import net.jacobpeterson.jet.openapiannotations.OpenApiPathItem; import net.jacobpeterson.jet.openapiannotations.OpenApiPaths; import net.jacobpeterson.jet.openapiannotations.OpenApiResponse; import net.jacobpeterson.jet.openapiannotations.OpenApiResponses; import net.jacobpeterson.jet.openapiannotations.OpenApiSchema; import net.jacobpeterson.jet.openapiannotations.OpenApiServer; import net.jacobpeterson.jet.openapiannotations.schemaname.SchemaName; import net.jacobpeterson.jet.server.JetServer; import net.jacobpeterson.jet.server.handle.Handle; import net.jacobpeterson.jet.server.route.simple.pathexact.PathExactRoute; import net.jacobpeterson.jet.server.router.simple.ImmutableSimpleRouter; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import static net.jacobpeterson.jet.common.http.header.contenttype.ContentType.APPLICATION_JSON_STRING; import static net.jacobpeterson.jet.common.http.method.Method.ToString.GET; import static net.jacobpeterson.jet.common.http.status.Status.OK_200; import static net.jacobpeterson.jet.openapiannotations.OpenApiParameter.ParameterLocation.QUERY; @NullMarked public class Server { @OpenApi( annotationGroupName = Web.PATH, info = @OpenApiInfo( title = "Web", version = "1.0.0" ), servers = @OpenApiServer(url = "http://localhost/" + Web.PATH) ) public static final class Web { public static final String PATH = "web"; private final @Getter AccountHandlers accountHandlers; public Web(final ImmutableSimpleRouter.Builder router) { accountHandlers = new AccountHandlers(router); } @SchemaName("Account") public static final class AccountHandlers { public AccountHandlers(final ImmutableSimpleRouter.Builder router) { router.addLast(PathExactRoute.builder() .method(GetInfo.METHOD) .path("/" + Web.PATH + GetInfo.PATH) .build(), this::getInfo); } public static final String TAG_NAME = "account"; public static final class GetInfo { public static final String METHOD = GET; public static final String PATH = "/" + TAG_NAME + "/info"; public static final String QUERY_KEY_ID = "id"; @Value @Builder public static class Success { String name; String email; @Nullable String profilePictureUrl; } public enum FailReason { NOT_LOGGED_IN, INVALID_ID } @Value @Builder public static class Response { @Nullable Success success; @Nullable FailReason failReason; } } @OpenApi(annotationGroupName = PATH, paths = @OpenApiPaths(@OpenApiPathItem.MapEntry( key = GetInfo.PATH, value = @OpenApiPathItem(methods = @OpenApiPathItem.MethodEntry( key = GetInfo.METHOD, value = @OpenApiOperation(tags = TAG_NAME, parameters = @OpenApiParameter( name = GetInfo.QUERY_KEY_ID, in = QUERY, required = true, schema = @OpenApiParameter.Schema(schema = @OpenApiSchema(fromClass = String.class)) ), responses = @OpenApiResponses({@OpenApiResponse.MapEntry( keyEnum = OK_200, value = @OpenApiResponse(content = @OpenApiMediaType.MapEntry( key = APPLICATION_JSON_STRING, value = @OpenApiMediaType(schema = @OpenApiSchema(fromClass = GetInfo.Response.class)) )) )}) )))))) public void getInfo(final Handle handle) { if (<check_logged_in_logic>) { handle.getResponse().responseJson(toJson(GetInfo.Response.builder() .failReason(GetInfo.FailReason.NOT_LOGGED_IN) .build())); return; } final var id = handle.getRequest().getUrl().getQueryValue(GetInfo.QUERY_KEY_ID); if (<check_id_logic>) { handle.getResponse().responseJson(toJson(GetInfo.Response.builder() .failReason(GetInfo.FailReason.INVALID_ID) .build())); return; } <get_account_info_logic> handle.getResponse().responseJson(toJson(GetInfo.Response.builder() .success(new GetInfo.Success(name, email, profilePictureUrl)) .build())); } } } static void main() { final var router = ImmutableSimpleRouter.builder(); new Web(router); JetServer.builder().router(router.build()).build(); } }
OpenAPI Annotations Plugin
A code-first OpenAPI specification annotations processor Gradle plugin.
This Gradle plugin generates OpenAPI specifications from the OpenAPI annotations declared in
your Java source code. Then you can use the
OpenAPI Generator Gradle plugin to generate OpenAPI client
libraries all within your Gradle build script. This creates an end-to-end solution for building APIs in a type-safe and
single-source-of-truth manner, all with great developer experience. For example, you have a model class called GetInfo
inside a class called AccountHandlers. The GetInfo class has the field @Nullable String firstName, but you want to
change it to String firstName denoting that it is no longer nullable. If you're using a generated OpenAPI TypeScript
client library, your TypeScript codebase will throw a compilation error for all the cases your frontend TypeScript code
uses firstName as a nullable value, allowing you to catch type-safety bugs for APIs at compile-time instead of at
runtime! This is how full-stack web development should have been all along!
Installation
For build.gradle.kts:
plugins {
id("net.jacobpeterson.jet.openapiannotationsplugin") version "3.3.0"
}For build.gradle:
plugins {
id 'net.jacobpeterson.jet.openapiannotationsplugin' version "3.3.0"
}There is no Maven plugin available at this time.
Guide
This Gradle plugin registers a task named jetOpenApiAnnotations and an extension also named jetOpenApiAnnotations
with the following configurations:
annotatedClassFiles = <files>classpaths = <files>schemaGeneratorConfigBuilderProvider = <SchemaGeneratorConfigBuilderProvider>schemaGeneratorUseNullableModule = <true or false>schemaGeneratorUseSchemaNameModule = <true or false>schemaGeneratorUseGsonModule = <true or false>schemaGeneratorUseJacksonModule = <true or false>schemaGeneratorSimpleTypeMappings = <Map<String, String>>generateOperationId = <DISABLED, FROM_CLASS_METHOD_NAME, FROM_METHOD_AND_PATH, BOTH>moveClassSchemasToComponents = <true or false>schemaValidation = <true or false>outputDirectory = <directory>outputDirectoryIncludeInJar = <true or false>
Example Configuration (build.gradle.kts)
jetOpenApiAnnotations {
schemaGeneratorUseGsonModule = true // Set to this `true` if you're using Gson
schemaGeneratorUseJacksonModule = false // Set to this `true` if you're using Jackson
// If you have a custom (de)serializer for `InetAddress` with Gson or Jackson, you can specify that every time
// `InetAddress` is used in a model class for an OpenAPI schema, it should be treated as a string.
schemaGeneratorSimpleTypeMappings.put("java.net.InetAddress", """{"type": "string"}""")
}Why not an annotation processor?
The main reason is that annotation processors cannot load classes if the class is part of the source code currently
being compiled, so
OpenApiSchema.fromClass()
would not be able to utilize the excellent
victools/jsonschema-generator library and the class file would
have to be manually inspected using TypeMirror. Additionally, annotation processors have various limitations, such as
no support for @CompileClasspath
for Gradle incremental builds.
Client
A simple, lightweight, modern, turnkey, Java web client library.
The Client module is a WIP and will be released soon! See issue #5.
Personal Note About AI
100% of this codebase was written by a human. 100% of this guide was written by a human. No AI slop is allowed here. I
care deeply about good engineering and excellent code quality. AI does not. So, do I use AI? Of course! But it will stay
inside my web browser as a chatbot where I'll ask it questions about specific problems, like a glorified StackOverflow.
I remain in the driver's seat. It's not in my IDE where an AI autocomplete is constantly suggesting average buggy code,
and certainly not where AI agents are mangling the codebase. Yes, AI autocompleted code can be reviewed, but I find
myself accepting lower quality code instead of critically thinking through each line and typing it myself, which almost
always yields higher quality code, but takes longer. LLMs fundamentally output the mean. I'm not saying that my code is
always perfect and better than AI, but I am saying that too many engineers are being sold a lie that if they aren't
constantly using AI, they will be left behind. Until there is a fundamental change in how LLMs work, this will remain my
opinion on the state of AI coding. </rant>
Contributions are very welcome, but keep these remarks in mind when submitting pull requests.
Alternatives
Please note that the below list focuses on the weaknesses of the alternative projects compared to Jet's strengths. Jet obviously has shortcomings compared to these other projects. Personal note: the below list is highly opinionated as a result of my work as a full-stack software engineer, almost always using a Java backend.
-
Jetty - excellent web server and client library (which is why Jet uses it), but has a complicated setup and lacks exhaustive Javadocs and header models
-
Javalin - awesome library, but requires Kotlin dependency and lacks header models and exhaustive KDocs
-
Spring - enterprise-grade, but bloated, uses lots of runtime annotation magic, and enforces an opinionated codebase structure
-
Vert.x Web - reactive programming is no longer necessary with the advent of Virtual Threads
-
Apache Tomcat - strict legacy Jakarta Servlet specification adherence
-
Spark Java - project abandoned
























