A lightweight Java 21 API framework inspired by FastAPI. Built directly on Jetty 12 (no servlets), with virtual threads, automatic JSON serialization, dependency injection, and OpenAPI documentation out of the box.
var app = new JavaFast();
app.get("/hello/{name}", req ->
Response.ok().json(Map.of("message", "Hello, " + req.pathVar("name") + "!"))
);
app.startAndWait(8080);- Lambda and annotation-based routing - define routes inline or in controller classes
- Route prefixes - mount controllers under a path prefix, like FastAPI's
include_router(prefix=...) - Typed routes - declare request/response types for automatic deserialization and OpenAPI generation
- Dependency injection - constructor injection and per-request
@Injectparameters, inspired by FastAPI'sDepends() - Middleware - composable request/response pipeline
- Virtual threads - enabled by default via Java 21
- OpenAPI + Swagger UI - auto-generated at
/docsand/openapi.json - Zero servlet overhead - runs directly on Jetty 12's core handler API
- Java 21+
- Gradle 9+
Add to your build.gradle:
dependencies {
implementation 'com.businessbay:javafast:0.1.0'
}import io.javafast.app.JavaFast;
import io.javafast.http.Response;
import java.util.Map;
public class App {
public static void main(String[] args) {
var app = new JavaFast();
app.get("/", req ->
Response.ok().json(Map.of("status", "running"))
);
app.startAndWait(8080);
}
}Register routes with the fluent API. Supported methods: get, post, put, delete, patch.
app.get("/users", req ->
Response.ok().json(userService.findAll())
);
app.post("/users", req -> {
var body = req.body(CreateUser.class);
var user = userService.create(body);
return Response.created().json(user);
});
app.delete("/users/{id}", req -> {
long id = req.pathVarAsLong("id");
userService.delete(id);
return Response.noContent();
});Define routes in controller classes using @JavaFastRoute:
import io.javafast.annotation.HttpMethod;
import io.javafast.annotation.JavaFastRoute;
class UserController {
@JavaFastRoute(path = "/users", method = HttpMethod.GET)
public Response list(JFRequest req) {
return Response.ok().json(userService.findAll());
}
@JavaFastRoute(path = "/users/{id}", method = HttpMethod.GET)
public Response getById(JFRequest req) {
long id = req.pathVarAsLong("id");
return Response.ok().json(userService.findById(id));
}
}
// Register the controller
app.scan(new UserController());Like FastAPI's include_router(prefix=...), you can mount controllers under a path prefix:
class AdminController {
@JavaFastRoute(path = "/users", method = HttpMethod.GET)
public Response listUsers(JFRequest req) { ... }
@JavaFastRoute(path = "/stats", method = HttpMethod.GET)
public Response stats(JFRequest req) { ... }
}
class PublicController {
@JavaFastRoute(path = "/health", method = HttpMethod.GET)
public Response health(JFRequest req) { ... }
}
// Routes become /admin/users, /admin/stats
app.scan("/admin", new AdminController());
// Routes become /api/v1/health
app.scan("/api/v1", new PublicController());
// Works with class-based DI scanning too
app.scan("/admin", AdminController.class);This keeps controller route definitions relative and reusable — the prefix is applied at registration time.
Controller methods can accept different parameter signatures:
| Signature | Description |
|---|---|
(JFRequest) -> Response |
Raw access to request and response |
(JFRequest) -> CustomType |
Return value auto-serialized to JSON |
(CustomType) -> Response |
Parameter auto-deserialized from JSON body |
() -> CustomType |
No-arg, return value auto-serialized |
Pass request/response types to enable OpenAPI schema generation:
record CreateUser(String name, String email) {}
record User(long id, String name, String email) {}
app.post("/users", CreateUser.class, User.class, (req, body) -> {
var user = userService.create(body.name(), body.email());
return Response.created().json(user);
});
app.get("/users", User.class, req ->
Response.ok().json(userService.findAll())
);Use {name} syntax in route paths:
app.get("/users/{id}/posts/{postId}", req -> {
long userId = req.pathVarAsLong("id");
long postId = req.pathVarAsLong("postId");
return Response.ok().json(postService.find(userId, postId));
});JFRequest wraps the incoming HTTP request:
// Path variables
req.pathVar("id") // String
req.pathVarAsInt("id") // int
req.pathVarAsLong("id") // long
// Query parameters
req.query("search") // String or null
req.query("search", "default") // String with default
req.queryAsInt("page", 1) // int with default
req.queryParams() // Map<String, String>
// Headers
req.header("Authorization")
req.contentType()
// Body
req.body() // raw String
req.body(CreateUser.class) // deserialized from JSON
// Metadata
req.method() // "GET", "POST", ...
req.path() // "/users/123"
req.ip() // client IP address
req.raw() // underlying Jetty RequestResponse is a fluent builder with factory methods for common status codes:
Response.ok() // 200
Response.created() // 201
Response.noContent() // 204
Response.badRequest() // 400
Response.unauthorized() // 401
Response.forbidden() // 403
Response.notFound() // 404
Response.error() // 500
Response.status(418) // customChain content methods:
Response.ok().json(myObject) // JSON (application/json)
Response.ok().body("plain text") // Plain text (text/plain)
Response.ok().html("<h1>Hello</h1>") // HTML (text/html)
Response.ok().header("X-Custom", "value").json(data) // Custom headersAdd middleware to intercept all requests. Middleware executes in registration order and wraps around the handler:
// Logging middleware
app.use((req, next) -> {
long start = System.currentTimeMillis();
Response res = next.proceed(req);
long ms = System.currentTimeMillis() - start;
System.out.printf("%s %s -> %d (%dms)%n",
req.method(), req.path(), res.getStatusCode(), ms);
return res;
});
// Auth middleware
app.use((req, next) -> {
if (req.path().startsWith("/api/") && req.header("Authorization") == null) {
return Response.unauthorized().json(Map.of("error", "missing token"));
}
return next.proceed(req);
});JavaFast includes a built-in DI system inspired by FastAPI's Depends(). Dependencies are resolved by type.
// Singleton — created once, shared across all requests
app.provide(DataSource.class, () -> createDataSource());
app.provide(UserRepository.class, () -> new JdbcUserRepository(createDataSource()));
// Request-scoped — created fresh for each request, receives the current JFRequest
app.providePerRequest(CurrentUser.class, req -> {
String token = req.header("Authorization");
return authService.verify(token);
});Pass controller classes (not instances) to scan(). The framework instantiates them, resolving constructor parameters from registered providers:
class UserController {
private final UserRepository repo;
// Constructor params resolved from the container
public UserController(UserRepository repo) {
this.repo = repo;
}
@JavaFastRoute(path = "/users", method = HttpMethod.GET)
public Response list(JFRequest req) {
return Response.ok().json(repo.findAll());
}
}
app.provide(UserRepository.class, () -> new JdbcUserRepository(ds));
app.scan(UserController.class); // framework creates the instanceConstructor-injected dependencies must be singletons. The constructor with the most parameters is used.
Use @Inject on method parameters to resolve request-scoped (or singleton) dependencies per request:
import io.javafast.annotation.Inject;
@JavaFastRoute(path = "/users", method = HttpMethod.GET)
public Response list(JFRequest req, @Inject CurrentUser user) {
// user is resolved from the container on each request
return Response.ok().json(repo.findByOwner(user.id()));
}You can mix JFRequest, @Inject parameters, and a body parameter in the same method:
@JavaFastRoute(path = "/items", method = HttpMethod.POST)
public Response create(CreateItemRequest body, JFRequest req, @Inject CurrentUser user) {
var item = repo.create(body, user.id());
return Response.created().json(item);
}Passing instances to scan() still works. Method-level @Inject is supported on both instance-scanned and class-scanned controllers. Prefixes work with all forms:
app.scan(new ManuallyCreatedController()); // manual instance
app.scan(InjectedController.class); // constructor injection
app.scan("/api/v1", new ManuallyCreatedController()); // manual instance + prefix
app.scan("/api/v1", InjectedController.class); // constructor injection + prefixOpenAPI 3.1 documentation is enabled by default:
/openapi.json- OpenAPI spec/docs- Interactive Swagger UI (via Scalar)
Configure or disable:
app.docs("My API", "1.0.0"); // set title and version
app.docs(false); // disable docs entirelyTyped routes and annotation-based controllers with typed parameters/returns automatically generate request/response schemas. Supported types in schema generation include records, enums, primitives, List<T>, Map<K, V>, LocalDate, LocalDateTime, Instant, and UUID.
Virtual threads are enabled by default (Java 21+). Every request runs on its own virtual thread via Jetty 12's executor. Disable if needed:
app.virtualThreads(false);app.start(8080); // start and return immediately
app.startAndWait(8080); // start and block the main thread
app.stop(); // stop the server
app.port(); // get the actual port (useful with port 0)import io.javafast.annotation.*;
import io.javafast.app.JavaFast;
import io.javafast.http.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
public class App {
record CreateItem(String name) {}
record Item(long id, String name) {}
record CurrentUser(long id, String email) {}
// Controller routes are defined relative — prefix is added at registration
static class ItemController {
private final Map<Long, Item> items = new ConcurrentHashMap<>();
private final AtomicLong idSeq = new AtomicLong(1);
@JavaFastRoute(path = "/items", method = HttpMethod.GET)
public Response list(JFRequest req) {
return Response.ok().json(items.values());
}
@JavaFastRoute(path = "/items", method = HttpMethod.POST)
public Response create(CreateItem body, @Inject CurrentUser user) {
long id = idSeq.getAndIncrement();
var item = new Item(id, body.name());
items.put(id, item);
return Response.created().json(item);
}
}
static class AdminController {
@JavaFastRoute(path = "/stats", method = HttpMethod.GET)
public Response stats(JFRequest req) {
return Response.ok().json(Map.of("uptime", "42h", "requests", 1337));
}
}
public static void main(String[] args) {
var app = new JavaFast();
// Middleware
app.use((req, next) -> {
long start = System.currentTimeMillis();
Response res = next.proceed(req);
System.out.printf("%s %s -> %d (%dms)%n",
req.method(), req.path(), res.getStatusCode(),
System.currentTimeMillis() - start);
return res;
});
// Dependencies
app.providePerRequest(CurrentUser.class, req -> {
// In a real app, validate the token
return new CurrentUser(1L, "user@example.com");
});
// Routes
app.get("/", req -> Response.ok().json(Map.of("status", "running")));
// Mount controllers with prefixes
app.scan("/api/v1", ItemController.class); // -> /api/v1/items
app.scan("/admin", new AdminController()); // -> /admin/stats
// Start
app.docs("Item API", "1.0.0");
app.startAndWait(8080);
}
}| Library | Version | Purpose |
|---|---|---|
| Jetty 12 | 12.0.16 | HTTP server (core handler API, no servlets) |
| Jackson | 2.18.2 | JSON serialization/deserialization |
| SLF4J + Logback | 2.0.16 / 1.5.15 | Logging |
MIT