DEV Community

Gregory Mitchell
Gregory Mitchell

Posted on

Java on Cloudflare Workers

This post details bytebox, a dependency and runtime to get Java to run on Serverless Architecture via Cloudflare Workers. It uses TeaVM to compile Java into WebAssembly usable by the Cloudflare Workers runtime.

It comes in multiple forms: you can use it as a Gradle plugin to output a deployable worker or a WebAssembly module, and comes as a NPM module to assemble workers by hand. Java dependencies are available via Maven Central and the Gradle Plugin Portal.

An important note about Cloudflare Workers vs Java is that Cloudflare Workers is a single-threaded synchronous state, so all operations are blocking and concurrency is not possible from Java. java.util.concurrent and other threading operations are not included and will result in a compilation error from trying to use them. The GitHub repository lists the full scope of other platform limitations and out-of-scope items, including the use of Process and ProcessBuilder, dynamic class loading, and inbound TCP listening or UDP handlers with DatagramSocket or ServerSocket.

Currently supported are Java 21 and 25 LTS versions. Documentation for both the Java API and the TypeScript API is available at bytebox.gmitch215.dev.

Quick Start

Copied from the README page:

package com.example;

import dev.gmitch215.bytebox.*;

public class HelloWorker implements Worker {
    @Override
    public Response fetch(Request request, Env env, ExecutionCtx ctx) {
        return Bytebox.response("hello from Java");
    }
}
Enter fullscreen mode Exit fullscreen mode

In your Gradle configuration:

plugins {
    java
    id("dev.gmitch215.bytebox") version "1.0.0"
}

repositories {
        mavenCentral()
}

dependencies { 
        implementation("dev.gmitch215:bytebox-core:1.0.0") 
}

// ...

bytebox {
    handlerClass = "com.example.HelloWorker" // to handler class

    wrangler {
        name = "hello-world" // name of cfw
        compatibilityDate = "2026-08-22" // compatibility date of cfw
    }
}
Enter fullscreen mode Exit fullscreen mode

The Gradle plugin will compile your code to WebAssembly using TeaVM and output the necessary worker scaffholding to use wrangler dev or wrangler deploy:

./gradlew buildWorker # build wrangler.jsonc, webassembly module, entrypoint
./gradlew workerDeploy # run wrangler deploy to logged-in account
Enter fullscreen mode Exit fullscreen mode

Bindings

All bindings available on Cloudflare Workers are supported in bytebox. The Gradle plugin allows you to easily add one or multiple bindings for your generated wrangler.jsonc, and provides default names if you so choose.

bytebox {
    bindings {
        kv()                             // KV
        kv("SESSIONS") { id = "abc123" } // an explicit name and a remote id
        d1()                             // DB
        r2()                             // BLOB
        durableObject("Counter")         // DO_COUNTER
    }
}
Enter fullscreen mode Exit fullscreen mode

You can also declare them like so:

bytebox {
    bindings(KV, D1, D1, KV) // KV, DB, DB_2, KV_2
}
Enter fullscreen mode Exit fullscreen mode

A Gradle task named bindingsReport is available to get detailed information about the bindings you have declared in your project.

JSON and Serialization

Many Cloudflare Workers handle and respond with JSON outputs. bytebox comes with buult-in utilities to parse and handle JSON.

To automatically serialize a class or a record, annotate it with @JsonType:

@JsonType
public record Order(String sku, int quantity, long total, List<String> tags) { // ... }
Enter fullscreen mode Exit fullscreen mode

Bytebox.json serializes it to JSON:

Order order = request.json(Order.class);
return Bytebox.json(order, Order.class);
Enter fullscreen mode Exit fullscreen mode

java.io is also supported normally, and requires no additional code.

byte[] wire = Serial.encode(order);
Order back = Serial.decode(wire, Order.class);
Enter fullscreen mode Exit fullscreen mode

Dependencies & Runtime

Java Standard Libraries

bytebox and TeaVM provide polyfills and runtime capabilities for you to use the majority of the Java Standard Library without any code modification. The Gradle plugin will automatically rewrite any class files or dependencies routed to a systems class that requires a polyfill, so no code modification is required.

ZonedDateTime local = Instant.now().atZone(ZoneId.of("America/New_York"));
HttpResponse<String> answer = HttpClient.newHttpClient()
    .send(HttpRequest.newBuilder(URI.create("https://example.com")).build(), ofString());
Enter fullscreen mode Exit fullscreen mode

Libraries like java.time, java.net, java.io, java.util.regex, and many others can be written like you would normally.

package com.example;

import dev.gmitch215.bytebox.Bytebox;
import dev.gmitch215.bytebox.Env;
import dev.gmitch215.bytebox.ExecutionCtx;
import dev.gmitch215.bytebox.Request;
import dev.gmitch215.bytebox.Response;
import dev.gmitch215.bytebox.Worker;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Ordinary Java, on a platform that has none of it.
 *
 * <p>Nothing here is a bytebox API. It is {@code java.time}, {@code java.net.http},
 * {@code java.util.regex} and {@code String.format}, written the way they are written anywhere, and
 * the compiler points each reference at an implementation that works on this runtime. That is what
 * makes an unmodified library compile: the library does not know it is being retargeted.
 *
 * <p>What each one costs, and where each one differs from a JVM, is in the technical report. The two
 * differences worth knowing at the call site are here as comments.
 */
public class StandardLibraryWorker implements Worker {

    private static final Pattern SINCE = Pattern.compile("since=(?<date>\\d{4}-\\d{2}-\\d{2})");

    private static final HttpClient CLIENT = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(5))
        .build();

    @Override
    public Response fetch(Request request, Env env, ExecutionCtx ctx) {
        LocalDate since = LocalDate.of(2026, 1, 1);
        Matcher asked = SINCE.matcher(request.getUrl());
        if (asked.find()) since = LocalDate.parse(asked.group("date"));

        // the clock is pinned between I/O, so this is the time the invocation began and does not move
        Instant now = Instant.now();
        ZonedDateTime local = now.atZone(ZoneId.of("America/New_York"));
        long days = ChronoUnit.DAYS.between(since, local.toLocalDate());

        StringBuilder body = new StringBuilder();
        body.append(local.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)).append('\n');
        // %,d groups every three digits here, which is right for the locales that group in threes
        body.append(String.format("%,d days since %s%n", days, since));
        body.append(String.format("%-12s %8.2f%%%n", "elapsed", (days * 100.0) / 365));
        body.append(upstream()).append('\n');

        return Bytebox.response(body.toString());
    }

    /** A request through the modern client, which is {@code fetch} underneath and suspends the fiber. */
    private String upstream() {
        try {
            HttpResponse<String> answer = CLIENT.send(
                HttpRequest.newBuilder(URI.create("https://example.com/"))
                    .header("Accept", "text/html")
                    .timeout(Duration.ofSeconds(3))
                    .build(),
                HttpResponse.BodyHandlers.ofString()
            );
            return String.format(
                "upstream %d, %,d bytes",
                answer.statusCode(),
                answer.body().length()
            );
        } catch (IOException | InterruptedException failed) {
            return "upstream unreachable: " + failed.getMessage();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

npm Dependencies

The bytebox Gradle plugin supports adding NPM dependencies to your worker, and comes with static analysis to generate supported types based on the npm module to use in your Java code.

bytebox {
    npm("nanoid", "^5.0.9")
    npmBindings("nanoid")
}
Enter fullscreen mode Exit fullscreen mode

npm installs the dependency, and npmBindings tells the plugin to read the *.d.ts declarations in the npm module to generate Java source files that you can use to work with the package.

You can also use the @JSBody annotation that TeaVM provides with the native keyword to wire it directly:

@JSBody(
    params = "size",
    imports = @JSBodyImport(alias = "nanoid", fromModule = "nanoid"),
    script = "return nanoid.nanoid(size);"
)
private static native String id(int size);
Enter fullscreen mode Exit fullscreen mode

Third-Party Java Dependencies

Dependencies can be declared as normal and they will be compiled in with the worker.

dependencies {
        implementation("com.example:foo:1.0.0")
}
Enter fullscreen mode Exit fullscreen mode

Cloudflare Workers' free plan has a hard limit of 3 MiB after gzip on the bundle, with paid increasing to 10 MiB after gzip, so installing heavy Java dependencies should be done with care. TeaVM only includes parts of the Java runtime that it detects is being used, and does not include the full runtime on compilation (unless everything is being used). The estimation for each runtime feature's cost is provided below.

Feature Added, gzipped
streams, collections, reflection 3.0 to 3.8 KB each
threads 6.8 KB
java.net.Socket 9.8 KB
BigDecimal 12.7 KB
java.util.regex 13.3 KB
java.net.URL and HttpURLConnection 13.3 KB
java.time with zones 23.8 KB
String.format 33.0 KB
java.net.http 54.1 KB

Cloudflare Runtime Libraries

Cloudflare Workers provides runtime libraries like cloudflare:sockets, cloudflare:mail, and many other built-ins available to you. bytebox provides Java API around these out of the box so you can use them how you please.

Examples

Borrowed from the GitHub repository's snippets.

Cron Job

All triggers are supported in bytebox, and cron is not an exception.

package com.example;

import dev.gmitch215.bytebox.Bytebox;
import dev.gmitch215.bytebox.Cron;
import dev.gmitch215.bytebox.Env;
import dev.gmitch215.bytebox.ExecutionCtx;
import dev.gmitch215.bytebox.Scheduled;
import dev.gmitch215.bytebox.builtin.Clock;

/**
 * A Worker with no HTTP handler at all.
 *
 * <p>Implementing only {@code Scheduled} means the generated Worker exports only {@code scheduled},
 * and the generated configuration carries only the trigger. Nothing is exported for a trigger the
 * handler does not implement.
 *
 * <p>An account gets 5 Cron Triggers on the free plan and 250 on paid, counted across every Worker
 * rather than per Worker. A scheduled invocation gets 15 minutes rather than a request's allowance,
 * and a throw is logged without a retry.
 */
public class NightlyWorker implements Scheduled {

    @Override
    public void scheduled(Cron cron, Env env, ExecutionCtx ctx) {
        Bytebox.log("fired for " + cron.expression() + ", due at " + Clock.iso(cron.scheduledAt()));
        env.kv().put("last-run", Clock.isoNow());
    }
}
Enter fullscreen mode Exit fullscreen mode

Durable Objects

Durable Objects are supported out-of-the-box with bytebox. They can either be used directly or be implemented over as a class.

package com.example;

import dev.gmitch215.bytebox.Bytebox;
import dev.gmitch215.bytebox.Env;
import dev.gmitch215.bytebox.ExecutionCtx;
import dev.gmitch215.bytebox.Request;
import dev.gmitch215.bytebox.Response;
import dev.gmitch215.bytebox.Worker;
import dev.gmitch215.bytebox.binding.DurableObjectNamespace;
import dev.gmitch215.bytebox.js.TSObject;

/**
 * Routes to a Durable Object, which is where state that has to be exact belongs.
 *
 * <p>One instance per id, in one place, so two requests for the same id reach the same instance and
 * see each other's writes. That is the difference from the kv-counter sample, where two regions can
 * both read the same value and both write the next one.
 *
 * <p>The Durable Object class itself is JavaScript: it extends Cloudflare's own base class, which is
 * a JavaScript class the runtime instantiates. What Java owns is the routing and the calls.
 */
public class CounterWorker implements Worker {

    @Override
    public Response fetch(Request request, Env env, ExecutionCtx ctx) {
        DurableObjectNamespace counters = env.durableObject("DO_COUNTER");

        // the id is derived from the path, so every path gets its own instance
        var counter = counters.byName(request.path());
        TSObject count = counter.rpc("increment");

        return Bytebox.response(request.path() + " is at " + count.asInt() + "\n");
    }
}
Enter fullscreen mode Exit fullscreen mode

Mail Routing

Cloudflare Workers can receive inbound mail, allowing code to handle and reroute any incoming mail it receives. The InboundMail class provided as apart of the implementation surface will automatically handle it based on the method you call, whether it is mail.forward, mail.reject, or anything else you choose to do with it.

package com.example;

import dev.gmitch215.bytebox.Bytebox;
import dev.gmitch215.bytebox.Env;
import dev.gmitch215.bytebox.ExecutionCtx;
import dev.gmitch215.bytebox.InboundMail;
import dev.gmitch215.bytebox.Mail;

/**
 * An inbound email router.
 *
 * <p>Cloudflare drops a message a handler returns without acting on, so {@code InboundMail} records
 * what was done and raises if nothing was. That makes the silent drop impossible to reach by
 * accident, and {@code drop()} is how to say the silence was meant.
 *
 * <p>Inbound messages are capped at 25 MiB. A reply needs the original to have passed DMARC, has to
 * come from the receiving domain, and is allowed once per message.
 */
public class MailRouter implements Mail {

    @Override
    public void email(InboundMail mail, Env env, ExecutionCtx ctx) {
        if (mail.rawSize() > 1_000_000) {
            mail.reject("messages over 1 MB are not accepted");
            return;
        }

        String sender = mail.from();
        if (env.kv().get("blocked:" + sender) != null) {
            mail.reject("this address is not accepted");
            return;
        }

        // acting and then carrying on is the point: the disposition is recorded on the message
        Bytebox.log("routing " + mail.rawSize() + " bytes from " + sender);
        env.kv().put("last-sender", sender);
        mail.forward("inbox@example.com");
    }
}
Enter fullscreen mode Exit fullscreen mode

Queue Consumer

Cloudflare Workers also supports using Queues as a trigger to handle multiple messages sent to the worker to be processed individually.

package com.example;

import dev.gmitch215.bytebox.Bytebox;
import dev.gmitch215.bytebox.Consumer;
import dev.gmitch215.bytebox.Env;
import dev.gmitch215.bytebox.ExecutionCtx;
import dev.gmitch215.bytebox.Message;
import dev.gmitch215.bytebox.MessageBatch;
import dev.gmitch215.bytebox.js.TSObject;

/**
 * A Queue consumer that acknowledges each message separately.
 *
 * <p>An uncaught exception retries the whole batch, which is rarely what a partial failure wants.
 * Acknowledging and retrying per message is how one bad message stops holding up the rest.
 *
 * <p>A body is any structured-clone value, so it arrives as a {@code TSObject}. A body with a known
 * shape can be read with a codec instead; see the {@code @JSONType} annotation.
 */
public class OrderConsumer implements Consumer<TSObject> {

    @Override
    public void queue(MessageBatch<TSObject> batch, Env env, ExecutionCtx ctx) {
        var insert = env.d1().prepare("insert into orders (id, sku) values (?, ?)");

        for (Message<TSObject> message : batch.messages()) {
            try {
                TSObject body = message.body();
                insert.bind(message.id(), body.get("sku").asString()).run();
                message.ack();
            } catch (RuntimeException failure) {
                Bytebox.log("order " + message.id() + " failed: " + failure.getMessage());
                // a fourth attempt is unlikely to go differently, so give up rather than loop
                if (message.attempts() >= 3) message.ack();
                else message.retry(30);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

HTTP API Router

bytebox comes with a Router class that makes it easy to use Java to make HTTP APIs. You can use them to build a tree of routes, similar to tools like Hono.

import dev.gmitch215.bytebox.http.Router;

public class Api implements Worker {

    private final Router routes = new Router()
        .filter(Api::requireToken)
        .get("/things", (request, env, ctx) -> Bytebox.json(env.d1().query("select * from things")))
        .get("/things/:id", (request, env, ctx) -> one(env, request.param("id")))
        .post("/things", Api::create);

    @Override
    public Response fetch(Request request, Env env, ExecutionCtx ctx) {
        return routes.handle(request, env, ctx);
    }

        // implement create(), requireToken(), and others below
}
Enter fullscreen mode Exit fullscreen mode

TCP Worker

The cloudflare:sockets module allows workers to send outbound TCP bytes to a specific hostname and port. No library is provided and only a raw connectTls function is provided, so libraries like edgeport can be used to wrap around SSH, SMTP, FTP, LDAP, and other TCP protocols you may need to use. Note that Cloudflare currently lists that it does not allow outbound TCP connections to its IP addresses or on port 22 directly.

package com.example;

import dev.gmitch215.bytebox.Bytebox;
import dev.gmitch215.bytebox.Env;
import dev.gmitch215.bytebox.ExecutionCtx;
import dev.gmitch215.bytebox.Request;
import dev.gmitch215.bytebox.Response;
import dev.gmitch215.bytebox.Worker;
import dev.gmitch215.bytebox.socket.Socket;
import dev.gmitch215.bytebox.socket.Sockets;

/**
 * A raw TCP client over {@code cloudflare:sockets}.
 *
 * <p>What this cannot reach decides whether it is the right tool. Cloudflare refuses a connection to
 * its own IP ranges, to localhost and to private addresses, and blocks port 25. So a Worker cannot
 * SMTP to Cloudflare Email Sending even with valid credentials; the email binding is the way out.
 *
 * <p>Every plan allows six simultaneous outgoing connections, so the socket is closed rather than
 * left to the runtime. Try-with-resources is why {@code Socket} is {@code AutoCloseable}.
 */
public class TcpWorker implements Worker {

    @Override
    public Response fetch(Request request, Env env, ExecutionCtx ctx) {
        String host = request.query("host", "example.com");

        try (Socket socket = Sockets.connectTLS(host, 443)) {
            socket.write("HEAD / HTTP/1.0\r\nHost: " + host + "\r\nConnection: close\r\n\r\n");
            String status = socket.readUntil("\r\n");
            return Bytebox.response(host + " answered " + status + "\n");
        } catch (RuntimeException refused) {
            // the refusal reads "proxy request failed, cannot connect to the specified address"
            return Bytebox.response("could not reach " + host + ": " + refused.getMessage(), 502);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Intermediate Example

package example;

import dev.gmitch215.bytebox.Env;
import dev.gmitch215.bytebox.ExecutionCtx;
import dev.gmitch215.bytebox.Request;
import dev.gmitch215.bytebox.Response;
import dev.gmitch215.bytebox.Worker;
import dev.gmitch215.bytebox.Bytebox;
import dev.gmitch215.bytebox.http.Filter;
import dev.gmitch215.bytebox.http.Route;
import dev.gmitch215.bytebox.http.Router;

import java.util.Map;
import java.util.Optional;

/**
 * A moderately realistic ByteBox Worker demonstrating:
 *
 * <ul>
 *     <li>HTTP routing</li>
 *     <li>Path parameters</li>
 *     <li>Query parameters</li>
 *     <li>Route-specific middleware</li>
 *     <li>Global middleware</li>
 *     <li>Authentication</li>
 *     <li>Request validation</li>
 *     <li>JSON responses</li>
 *     <li>404 handling</li>
 *     <li>Basic error handling</li>
 * </ul>
 *
 * <p>The handlers deliberately use ordinary Java control flow. ByteBox's
 * Worker runtime handles suspension when a handler performs supported I/O.</p>
 */
public final class ApiWorker implements Worker {

    private final Router router;

    public ApiWorker() {
        this.router = buildRouter();
    }

    /**
     * Worker entry point.
     *
     * <p>All requests are handed to the Router. The router is responsible
     * for matching the HTTP method/path and executing middleware before
     * invoking the selected handler.</p>
     */
    @Override
    public Response fetch(
            Request request,
            Env env,
            ExecutionCtx ctx
    ) {
        try {
            return router.route(request, env, ctx);
        } catch (Exception e) {
            // In production, log the exception rather than returning its
            // details to the client.
            Bytebox.log("Unhandled request error: " + e);

            return Bytebox.json(
                    """
                    {
                      "error": "internal_server_error"
                    }
                    """,
                    500
            );
        }
    }

    /**
     * Constructs the application router.
     *
     * <p>Middleware registered with {@code use()} runs for matching requests
     * before the route handler. Route-specific filters can additionally be
     * supplied when registering an individual route.</p>
     */
    private Router buildRouter() {
        Router router = Router.create();

        /*
         * ------------------------------------------------------------------
         * Global middleware
         * ------------------------------------------------------------------
         */

        // Add a request ID to every request and expose it through the
        // response headers. In a larger application this could instead
        // attach information to a request context abstraction.
        router.use(requestIdMiddleware());

        // Reject unsupported HTTP methods before route dispatch.
        router.use(methodGuardMiddleware());

        /*
         * ------------------------------------------------------------------
         * Public routes
         * ------------------------------------------------------------------
         */

        router.get(
                "/",
                (request, path, env, ctx) ->
                        Bytebox.json(
                                """
                                {
                                  "name": "example-api",
                                  "status": "ok"
                                }
                                """
                        )
        );

        router.get(
                "/health",
                this::health
        );

        /*
         * Query parameters can be read from the request URL.
         *
         * Example:
         *
         *     GET /users?limit=20
         *
         * The exact URL/query representation comes from ByteBox's Request
         * abstraction, while the rest of the handler remains ordinary Java.
         */
        router.get(
                "/users",
                this::listUsers
        );

        /*
         * ------------------------------------------------------------------
         * Authenticated routes
         * ------------------------------------------------------------------
         *
         * Authentication is applied only to this group of routes rather
         * than globally, so /health and / can remain public.
         */

        router.get(
                "/me",
                authenticated(),
                this::currentUser
        );

        router.get(
                "/users/:id",
                authenticated(),
                this::getUser
        );

        router.post(
                "/users",
                authenticated(),
                this::createUser
        );

        /*
         * ------------------------------------------------------------------
         * Admin routes
         * ------------------------------------------------------------------
         *
         * Multiple filters can be composed. Authentication runs first;
         * authorization runs only after authentication succeeds.
         */

        router.delete(
                "/users/:id",
                authenticated(),
                requireRole("admin"),
                this::deleteUser
        );

        /*
         * ------------------------------------------------------------------
         * Wildcard example
         * ------------------------------------------------------------------
         *
         * A wildcard route can be useful for proxy-like or asset-like
         * endpoints.
         */
        router.get(
                "/debug/*",
                authenticated(),
                requireRole("admin"),
                this::debug
        );

        /*
         * ------------------------------------------------------------------
         * 404
         * ------------------------------------------------------------------
         *
         * Router invokes this handler when no registered route matches.
         */
        router.notFound(
                (request, path, env, ctx) ->
                        Bytebox.json(
                                """
                                {
                                  "error": "not_found"
                                }
                                """,
                                404
                        )
        );

        return router;
    }

    /**
     * Simple request-ID middleware.
     *
     * <p>Middleware receives a {@link java.util.function.Supplier} for the
     * next handler. Calling {@code next.get()} continues the request.</p>
     */
    private Filter requestIdMiddleware() {
        return (request, path, env, ctx, next) -> {
            String requestId = Bytebox.uuid();

            Response response = next.get();

            /*
             * The Response API exposes its headers, allowing middleware
             * to add information to an otherwise completed response.
             */
            response.getHeaders().set("X-Request-ID", requestId);

            return response;
        };
    }

    /**
     * Example global middleware that rejects methods which the application
     * does not intend to support.
     *
     * <p>Route matching still determines whether a particular endpoint
     * exists; this filter merely demonstrates application-level policy.</p>
     */
    private Filter methodGuardMiddleware() {
        return (request, path, env, ctx, next) -> {
            String method = request.getMethod();

            if (method == null) {
                return Bytebox.json(
                        """
                        {
                          "error": "invalid_request"
                        }
                        """,
                        400
                );
            }

            return switch (method) {
                case "GET", "POST", "PUT", "PATCH", "DELETE" ->
                        next.get();

                default ->
                        Bytebox.json(
                                """
                                {
                                  "error": "method_not_allowed"
                                }
                                """,
                                405
                        );
            };
        };
    }

    /**
     * Authentication middleware.
     *
     * <p>This example intentionally keeps authentication simple. A real
     * implementation could validate a JWT, call an identity service, or
     * inspect a signed session cookie.</p>
     */
    private Filter authenticated() {
        return (request, path, env, ctx, next) -> {
            String authorization = request.getHeaders().get("Authorization");

            if (authorization == null ||
                    !authorization.startsWith("Bearer ")) {

                return Bytebox.json(
                        """
                        {
                          "error": "unauthorized"
                        }
                        """,
                        401
                );
            }

            String token = authorization.substring("Bearer ".length());

            if (!isValidToken(token)) {
                return Bytebox.json(
                        """
                        {
                          "error": "invalid_token"
                        }
                        """,
                        401
                );
            }

            /*
             * ByteBox does not require middleware to be a particular
             * authentication framework. Application state can be carried
             * through whatever application-level context abstraction the
             * application uses.
             *
             * This example simply demonstrates successful middleware
             * continuation.
             */
            return next.get();
        };
    }

    /**
     * Authorization middleware.
     *
     * <p>In a production application, the user's authenticated identity
     * would normally come from a request context established by the
     * authentication layer.</p>
     */
    private Filter requireRole(String requiredRole) {
        return (request, path, env, ctx, next) -> {
            /*
             * Demo-only authorization decision.
             *
             * Replace this with the application's actual identity/context
             * lookup.
             */
            String role = request.getHeaders().get("X-Debug-Role");

            if (!requiredRole.equals(role)) {
                return Bytebox.json(
                        """
                        {
                          "error": "forbidden"
                        }
                        """,
                        403
                );
            }

            return next.get();
        };
    }

    /**
     * GET /health
     */
    private Response health(
            Request request,
            Map<String, String> path,
            Env env,
            ExecutionCtx ctx
    ) {
        return Bytebox.json(
                """
                {
                  "status": "healthy"
                }
                """
        );
    }

    /**
     * GET /users
     *
     * <p>Demonstrates reading query parameters and producing a JSON
     * response. The application could replace the in-memory list with
     * a D1 query without changing the routing structure.</p>
     */
    private Response listUsers(
            Request request,
            Map<String, String> path,
            Env env,
            ExecutionCtx ctx
    ) {
        String url = request.getUrl();

        int limit = extractLimit(url);

        if (limit < 1 || limit > 100) {
            return Bytebox.json(
                    """
                    {
                      "error": "limit_must_be_between_1_and_100"
                    }
                    """,
                    400
            );
        }

        /*
         * Normally:
         *
         *     env.d1()
         *         .prepare("SELECT ... LIMIT ?")
         *         .bind(limit)
         *         .all();
         *
         * would be used here.
         */
        String json = """
                {
                  "users": [
                    {
                      "id": 1,
                      "name": "Ada"
                    },
                    {
                      "id": 2,
                      "name": "Grace"
                    }
                  ],
                  "limit": %d
                }
                """.formatted(limit);

        return Bytebox.json(json);
    }

    /**
     * GET /me
     */
    private Response currentUser(
            Request request,
            Map<String, String> path,
            Env env,
            ExecutionCtx ctx
    ) {
        return Bytebox.json(
                """
                {
                  "id": 42,
                  "name": "Example User",
                  "role": "admin"
                }
                """
        );
    }

    /**
     * GET /users/:id
     *
     * <p>The {@code path} map contains the named route parameter:
     *
     * <pre>
     *     /users/:id
     *           ^
     *           └── path.get("id")
     * </pre>
     */
    private Response getUser(
            Request request,
            Map<String, String> path,
            Env env,
            ExecutionCtx ctx
    ) {
        String id = path.get("id");

        if (id == null || id.isBlank()) {
            return Bytebox.json(
                    """
                    {
                      "error": "missing_user_id"
                    }
                    """,
                    400
            );
        }

        if (!id.matches("\\d+")) {
            return Bytebox.json(
                    """
                    {
                      "error": "user_id_must_be_numeric"
                    }
                    """,
                    400
            );
        }

        /*
         * A real D1-backed implementation could do:
         *
         *     var result = env.d1()
         *         .prepare("SELECT id, name FROM users WHERE id = ?")
         *         .bind(Long.parseLong(id))
         *         .first();
         */

        return Bytebox.json(
                """
                {
                  "id": %s,
                  "name": "Example User"
                }
                """.formatted(id)
        );
    }

    /**
     * POST /users
     *
     * <p>Demonstrates reading a request body as JSON. The actual application
     * would normally deserialize into a generated ByteBox JSON codec type.</p>
     */
    private Response createUser(
            Request request,
            Map<String, String> path,
            Env env,
            ExecutionCtx ctx
    ) {
        String body = request.text();

        if (body == null || body.isBlank()) {
            return Bytebox.json(
                    """
                    {
                      "error": "request_body_required"
                    }
                    """,
                    400
            );
        }

        /*
         * Example body:
         *
         * {
         *   "name": "Linus"
         * }
         *
         * For a real application, prefer a @JSONType-generated codec instead
         * of manually parsing strings.
         */

        if (!body.contains("\"name\"")) {
            return Bytebox.json(
                    """
                    {
                      "error": "name_required"
                    }
                    """,
                    422
            );
        }

        /*
         * A real implementation could insert into D1 here and return the
         * newly-created resource.
         */
        Response response = Bytebox.json(
                """
                {
                  "id": 100,
                  "name": "New User"
                }
                """,
                201
        );

        response.getHeaders().set(
                "Location",
                "/users/100"
        );

        return response;
    }

    /**
     * DELETE /users/:id
     *
     * <p>Authentication and admin authorization have already been handled
     * by route-specific middleware before this method executes.</p>
     */
    private Response deleteUser(
            Request request,
            Map<String, String> path,
            Env env,
            ExecutionCtx ctx
    ) {
        String id = path.get("id");

        if (id == null || !id.matches("\\d+")) {
            return Bytebox.json(
                    """
                    {
                      "error": "invalid_user_id"
                    }
                    """,
                    400
            );
        }

        /*
         * Real implementation:
         *
         *     env.d1()
         *         .prepare("DELETE FROM users WHERE id = ?")
         *         .bind(Long.parseLong(id))
         *         .run();
         */

        return Bytebox.status(204);
    }

    /**
     * GET /debug/*
     *
     * <p>Demonstrates a wildcard route. The router places the matched
     * path information into the route parameter map.</p>
     */
    private Response debug(
            Request request,
            Map<String, String> path,
            Env env,
            ExecutionCtx ctx
    ) {
        String wildcard = path.get("*");

        return Bytebox.json(
                """
                {
                  "debug": true,
                  "path": %s
                }
                """.formatted(
                        jsonString(wildcard == null ? "" : wildcard)
                )
        );
    }

    /**
     * Demo token validator.
     *
     * <p>Replace this with actual signature verification / identity lookup.
     * No secret is embedded in application code.</p>
     */
    private boolean isValidToken(String token) {
        return !token.isBlank() && token.length() >= 16;
    }

    /**
     * Minimal query-string extraction used only to keep the example focused
     * on Router rather than URL parsing.
     *
     * <p>For production code, use ByteBox's URL/request facilities rather
     * than relying on this deliberately small helper.</p>
     */
    private int extractLimit(String url) {
        if (url == null) {
            return 20;
        }

        int queryStart = url.indexOf('?');

        if (queryStart < 0) {
            return 20;
        }

        String query = url.substring(queryStart + 1);

        for (String parameter : query.split("&")) {
            String[] parts = parameter.split("=", 2);

            if (parts.length == 2 && parts[0].equals("limit")) {
                try {
                    return Integer.parseInt(parts[1]);
                } catch (NumberFormatException ignored) {
                    return -1;
                }
            }
        }

        return 20;
    }

    /**
     * Escapes a string sufficiently for this small JSON demonstration.
     *
     * <p>Use ByteBox's generated JSON codecs for real application models.</p>
     */
    private String jsonString(String value) {
        if (value == null) {
            return "null";
        }

        return "\"" + value
                .replace("\\", "\\\\")
                .replace("\"", "\\\"")
                .replace("\n", "\\n")
                .replace("\r", "\\r")
                .replace("\t", "\\t")
                + "\"";
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

This was a short demonstration about how to use bytebox to get Java to run on Cloudflare Workers. You can use it to interop between JavaScript and Java on serverless state to maximize the capabilities of your applications.

Top comments (0)