Skip to content

willbackslash/javafast

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JavaFast

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);

Features

  • 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 @Inject parameters, inspired by FastAPI's Depends()
  • Middleware - composable request/response pipeline
  • Virtual threads - enabled by default via Java 21
  • OpenAPI + Swagger UI - auto-generated at /docs and /openapi.json
  • Zero servlet overhead - runs directly on Jetty 12's core handler API

Requirements

  • Java 21+
  • Gradle 9+

Installation

Add to your build.gradle:

dependencies {
    implementation 'com.businessbay:javafast:0.1.0'
}

Quick Start

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);
    }
}

Routing

Lambda Routes

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();
});

Annotation-Based Controllers

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());

Route Prefixes

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

Typed Routes

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())
);

Path Variables

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));
});

Request

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 Request

Response

Response 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)    // custom

Chain 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 headers

Middleware

Add 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);
});

Dependency Injection

JavaFast includes a built-in DI system inspired by FastAPI's Depends(). Dependencies are resolved by type.

Registering Providers

// 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);
});

Constructor Injection

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 instance

Constructor-injected dependencies must be singletons. The constructor with the most parameters is used.

Method Parameter Injection

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);
}

Backward Compatibility

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 + prefix

OpenAPI Documentation

OpenAPI 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 entirely

Typed 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

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);

Server Lifecycle

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)

Full Example

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);
    }
}

Dependencies

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

License

MIT

About

an inspired fastapi for java micro web framework

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages