This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
moleculer-java-web is the API Gateway for moleculer-java.
It is a Moleculer Service (ApiGateway) that exposes other Moleculer services over HTTP/REST and
WebSocket. It is server-independent: the same gateway runs behind a standalone Netty server or
inside any Jakarta servlet container (Jetty 12+, Tomcat 10+, WildFly, …). Published to Maven Central as
com.github.berkesa:moleculer-java-web; the Maven build names the JAR after the artifactId
(moleculer-java-web-<version>.jar).
All data — request params, action config, responses — flows as io.datatree.Tree (the JSON-like
structure from the datatree library that the whole Moleculer ecosystem uses).
Maven build (one pom.xml); bytecode target Java 17 (<maven.compiler.release>17</maven.compiler.release>),
build JDK 17+ (JDK 25 in use), minimum consumer runtime JDK 17 (Spring 6 direct dep),
compiled with javac. Version is 2.1.0.
mvn clean verify # compile + run all tests
mvn clean install # also install moleculer-java-web-<version>.jar into the local ~/.m2
mvn -Prelease clean deploy # sources + javadoc + GPG + Central Portal publish (release profile)
# run a single test class
mvn test -Dtest=NettyTest
# run a single test method
mvn test -Dtest='NettyTest#testMiddlewares'Sample.java (in src/test/java) has a main() showing the minimal standalone setup; run it (or the
.vscode/launch.json "Sample" config) to start a local Netty gateway on port 8080.
- Resource quirk: only
**/*.icofiles undersrc/main/javaare bundled into the jar (used by theFaviconmiddleware) — see<build><resources>inpom.xml. Test fixtures (templates, html, message bundles,moleculer.config.xml) live undersrc/test/javaand are picked up via<testResources>. - Connector / servlet / WebSocket API deps are
provided; template engines + the multipart parser are<optional>— a consumer pulls only the connector and template engine it actually uses. Netty +moleculer-javaare normal compile deps. - Tests bind
127.0.0.1:3000(Netty + embedded Jetty 12) and run sequentially;AbstractTemplateTestwaits for the port to be free before each bind. They run fully offline (no external broker).
The core abstraction is RequestProcessor.service(WebRequest req, WebResponse rsp). WebRequest/WebResponse
hide whether the request came from Netty or a servlet, so nothing below the connector layer knows about
either. The flow:
-
Connector adapts the native request to
WebRequest/WebResponseand callsApiGateway.service(...):- Netty —
NettyServerbuilds the channel pipeline;MoleculerHandlerwraps requests asNettyWebRequest/NettyWebResponse. Standalone, non-blocking, supports SSL (JDK or OpenSSL) and WebSocket. Slowloris hardening:NettyServer.setReadTimeout(seconds)(off by default,0) inserts anIdleStateHandlerso connections that stall mid-request are closed;MoleculerHandler'suserEventTriggered/channelInactive/exceptionCaughtrelease the half-openreq.stream, and the idle handler is removed from the pipeline on a successful WebSocket upgrade (long-lived sockets are managed byNettyWebSocketRegistryinstead). SeeNettySlowRequestTest. - Servlet —
MoleculerServletboots a Spring 6 context (Spring Boot viamoleculer.application, or XML viamoleculer.config), finds theApiGateway, and auto-detects blocking vs non-blocking mode (AsyncServicewhen async I/O is available — detected viaClass.forName("jakarta.servlet.ReadListener")— elseBlockingService; falls back to blocking onIllegalStateException, and forces blocking on WebLogic). The servlet/WebSocket layer is Jakarta (jakarta.servlet.*/jakarta.websocket.*); the J2EE WebSocket connector (ServletWebSocketRegistry+ thewebsocketpackage) targets the JSR-356 API supplied by a Jakarta container (e.g. Jetty 12 ee10). Request read timeouts are owned by the container; for non-blocking mode themoleculer.async.timeoutinit-param (ms,0= container default) caps the async context (ServiceMode.setAsyncTimeout→AsyncContext.setTimeout), andNonBlockingWebRequestregisters anAsyncListenerthat aborts the body stream on timeout/error.
- Netty —
-
ApiGateway.service(...)resolves the request to aMapping:- looks in the static mapping cache (exact
METHOD pathkey, e.g.GET /user), then the dynamic mapping cache (parameterized paths). Both caches are guarded by aReentrantReadWriteLockand bounded bycachedRoutes(default 2048). - on a miss, walks the
Route[], thenlastRoute(which runslastMiddleware, defaultNotFound= 404), and caches the resulting mapping.
- looks in the static mapping cache (exact
-
Routeturns an incoming(method, path)into aMappingviafindMapping(...), using, in order: aliases → whitelist patterns → mapping policy. A route carries its own middlewares, optional template engine,beforeCall/afterCallhooks, and executor. -
Mappingbuilds and owns the middleware chain. The chain is constructed inside-out:ActionInvoker(the terminal processor that actually calls the Moleculer action and serializes the result) is the innermostparent; eachHttpMiddleware.install(next, config)wraps the current head and becomes the new head (lastProcessor). A request enters at the outermost middleware and unwinds towardActionInvoker.
- Alias — explicit
httpMethod + pathPattern -> actionName.ALLmatches any method. ARESTalias auto-expands into 5 CRUD aliases:GET list,GET /:id get,POST create,PUT /:id update,DELETE /:id remove. - Whitelist +
MappingPolicy—RESTRICT(default) exposes only whitelisted name patterns;ALLexposes everything. When matched by whitelist/policy, the path is converted to an action name:/→.,~→$. - Path patterns — static (
/user), wildcard (/files*), or parameterized (/user/:id, compiled to a regex with a per-mapping matcher cache). Static mappings cache far more cheaply than dynamic ones. @HttpAliasannotation on a service's action field — the gateway auto-deploys these by subscribing to the$services.changedevent (autoDeployListenerinApiGateway), so annotated actions appear as routes without manualaddRoute/addAliascalls.
Every middleware extends HttpMiddleware (which is itself a Moleculer Service, so it has lifecycle
started/stopped) and implements install(next, config) returning a RequestProcessor that delegates to
next. Added globally via ApiGateway.use(...) or per-route via Route.use(...). Notable ones: CorsHeaders,
BasicAuthenticator, RateLimiter (pluggable RatingStore), ServeStatic, Favicon, SessionHandler
(pluggable SessionStore), ResponseDeflater (gzip), Redirector, ErrorPage, NotFound, IpFilter,
HostNameFilter, XSRFToken, ResponseTime, ResponseTimeout, RequestLogger, TopLevelCache.
Pluggable server-side HTML rendering, all extending AbstractTemplateEngine: DataTree, FreeMarker,
Mustache, Thymeleaf, Pebble, Handlebars, Velocity. Set globally on the gateway (setTemplateEngine) or per route. Each engine's
library is an <optional> dependency in pom.xml, so a consumer only needs the one it actually uses.
WebSocketRegistry (Netty: NettyWebSocketRegistry; Servlet: ServletWebSocketRegistry) tracks client
connections per path. Server-to-client push is event-driven: broadcast the Moleculer event websocket.send
with { path, data } and the gateway's webSocketListener forwards it to matching sockets. WebSocketFilter
gates which clients may connect.
CallProcessor beforeCall/afterCall (mutate the request Tree/response around the action call) and a
custom ExecutorService can be set on the ApiGateway (applied to all routes) or per Route.
A gateway only works when deployed into a ServiceBroker alongside a connector service. Minimal Netty setup:
new ServiceBroker()
.createService(new NettyServer(8080)) // connector
.createService(new ApiGateway("**")) // gateway; "**" whitelists all services
.createService(new Service("math") { Action add = ctx ->
ctx.params.get("a", 0) + ctx.params.get("b", 0); })
.start();
// GET http://localhost:8080/math/add?a=3&b=6Tests are JUnit 5 (Jupiter). In NettyTest, BlockingServletTest, NonBlockingServletTest (and the
WebSocket tests NettyWebSocketTest / JettyWebSocketTest), the broker is built with a ConstantMonitor,
the connector and ApiGateway are created on it. The servlet tests run on an embedded Jetty 12 (ee10)
container. AbstractTemplateTest is the shared base: subclasses implement startServer()/stopServer()
(so the connector is built before the routes are installed), and it drives HTTP requests with the
httpclient5 async client (org.apache.hc.client5). The rate-limiter loop uses a plain HttpURLConnection
to keep its rapid sequential requests inside the 1-second window. Subclass AbstractTemplateTest to reuse
its assertions against a different connector/mode.