AI Coding Instructions

Copy pre-built instructions to teach your AI coding assistant about Javalin 7.1.0. Each block contains the same core knowledge — API patterns, conventions, and common pitfalls — adapted to the format your tool expects. Add the instructions to your project's AI config file and your assistant will write better Javalin code immediately.

Claude Code

# ╔══════════════════════════════════════════════════════════════╗
# ║              JAVALIN FRAMEWORK INSTRUCTIONS                  ║
# ╚══════════════════════════════════════════════════════════════╝
# Javalin 7.1.0 — AI Coding Instructions

## Project overview
Javalin is a lightweight web framework for Java and Kotlin, built on Jetty 12.
- Requires Java 17+
- Maven: `io.javalin:javalin:7.1.0`
- Bundle (includes Jackson, Logback, testing): `io.javalin:javalin-bundle:7.1.0`
- SLF4J is the only required dependency — add Logback or another implementation for logging

## Core pattern
All configuration (routes, plugins, lifecycle) goes inside `Javalin.create(config -> { ... })`:

```java
// Java
import io.javalin.Javalin;

void main() {
    var app = Javalin.create(config -> {
        config.routes.get("/", ctx -> ctx.result("Hello World"));
    }).start(7070);
}
```

```kotlin
// Kotlin
import io.javalin.Javalin

fun main() {
    val app = Javalin.create { config ->
        config.routes.get("/") { ctx -> ctx.result("Hello World") }
    }.start(7070)
}
```

## Routing
Routes are defined via `config.routes`:

```java
config.routes.get("/users", ctx -> ctx.json(userDao.getAll()));
config.routes.get("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    ctx.json(userDao.getById(id));
});
config.routes.post("/users", ctx -> {
    User user = ctx.bodyAsClass(User.class);
    userDao.create(user);
    ctx.status(201);
});
config.routes.put("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    User user = ctx.bodyAsClass(User.class);
    userDao.update(id, user);
});
config.routes.delete("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    userDao.delete(id);
});
```

Supported methods: `get`, `post`, `put`, `patch`, `delete`, `query`, `head`, `options`.

Path parameters: `{param}` (no slashes) or `<param>` (allows slashes).
Wildcard: `/path/*` matches anything (but value cannot be extracted — use `<param>` instead).

## Handlers
The `Handler` interface is `ctx -> { ... }` with a void return. Set the response with:
- `ctx.result("text")` — plain text
- `ctx.json(object)` — JSON (requires Jackson or another JSON mapper)
- `ctx.html("<h1>Hi</h1>")` — HTML
- `ctx.status(code)` — HTTP status
- `ctx.redirect("/path")` — redirect
- `ctx.future(completableFuture)` — async

## Before/after handlers
```java
config.routes.before(ctx -> { /* runs before every request */ });
config.routes.after(ctx -> { /* runs after every request */ });
config.routes.beforeMatched(ctx -> { /* only if a route matched */ });
config.routes.afterMatched(ctx -> { /* only if a route matched */ });
config.routes.before("/api/*", ctx -> { /* path-scoped */ });
```

## Validation
```java
// Path parameter validation
int id = ctx.pathParamAsClass("id", Integer.class)
    .check(i -> i > 0, "ID must be positive")
    .get();

// Query parameter validation
int page = ctx.queryParamAsClass("page", Integer.class)
    .getOrDefault(1);

// Body validation
User user = ctx.bodyValidator(User.class)
    .check(u -> u.name != null, "Name required")
    .get();
```

## WebSockets
```java
config.routes.ws("/websocket", ws -> {
    ws.onConnect(ctx -> { /* WsConnectContext */ });
    ws.onMessage(ctx -> { ctx.send("Echo: " + ctx.message()); });
    ws.onClose(ctx -> { /* WsCloseContext */ });
    ws.onError(ctx -> { /* WsErrorContext */ });
});
```

## Server-Sent Events
```java
config.routes.sse("/sse", client -> {
    client.sendEvent("message", "Hello SSE");
    client.onClose(() -> { /* cleanup */ });
    client.keepAlive();
});
```

## Exception and error mapping
```java
config.error.exception(NotFoundException.class, (e, ctx) -> {
    ctx.status(404).result(e.getMessage());
});
config.error.error(404, ctx -> {
    ctx.result("Page not found");
});
```

## Access management
```java
config.accessManager((handler, ctx, routeRoles) -> {
    Role userRole = getUserRole(ctx);
    if (routeRoles.contains(userRole)) {
        handler.handle(ctx);
    } else {
        ctx.status(403).result("Forbidden");
    }
});
```

## Plugin configuration
```java
Javalin.create(config -> {
    // Bundled plugins
    config.bundledPlugins.enableCors(cors -> cors.addRule(it -> it.anyHost()));
    config.bundledPlugins.enableRouteOverview("/routes");
    config.bundledPlugins.enableDevLogging();

    // Static files
    config.staticFiles.add("/public", Location.CLASSPATH);
});
```

Available add-on artifacts: `javalin-rendering-{engine}` (JTE, Thymeleaf, Velocity, Pebble, Mustache, Handlebars), `javalin-micrometer`, `javalin-ssl`.

Custom plugins implement the `Plugin` interface.

## Handler groups
Use `apiBuilder` to group routes by path prefix (requires `import static io.javalin.apibuilder.ApiBuilder.*`):

```java
config.routes.apiBuilder(() -> {
    path("/users", () -> {
        get(UserController::getAllUsers);
        post(UserController::createUser);
        path("/{id}", () -> {
            get(UserController::getUser);
            patch(UserController::updateUser);
            delete(UserController::deleteUser);
        });
    });
});
```

CrudHandler shortcut — maps `getAll`, `getOne`, `create`, `update`, `delete` automatically:
```java
config.routes.apiBuilder(() -> {
    crud("users/{user-id}", new UserCrudHandler());
});
```

## Default HTTP responses
Throw typed exceptions for standard error responses (JSON body if client accepts JSON):
- `throw new BadRequestResponse("message")` — 400
- `throw new UnauthorizedResponse("message")` — 401
- `throw new ForbiddenResponse("message")` — 403
- `throw new NotFoundResponse("message")` — 404
- `throw new MethodNotAllowedResponse("message")` — 405
- `throw new ConflictResponse("message")` — 409
- `throw new GoneResponse("message")` — 410
- `throw new InternalServerErrorResponse("message")` — 500

All extend `HttpResponseException`. You can pass additional details: `new BadRequestResponse("msg", Map.of("detail", "value"))`.

## File uploads
```java
config.routes.post("/upload", ctx -> {
    UploadedFile file = ctx.uploadedFile("myFile");
    // file.filename(), file.content() (InputStream), file.size(), file.contentType()
    FileUtil.streamToFile(file.content(), "upload/" + file.filename());
});
// Multiple files
ctx.uploadedFiles("files"); // List<UploadedFile>
```

## Template rendering
Add a rendering engine artifact, e.g. `io.javalin:javalin-rendering-jte:7.1.0`:

```java
// Register the renderer
config.fileRenderer(new JavalinJte());

// Use in a handler
config.routes.get("/hello", ctx -> {
    ctx.render("hello.jte", Map.of("name", "World"));
});
```

Available engines: `javalin-rendering-jte`, `javalin-rendering-thymeleaf`, `javalin-rendering-velocity`, `javalin-rendering-pebble`, `javalin-rendering-mustache`, `javalin-rendering-handlebars`, `javalin-rendering-freemarker`, `javalin-rendering-commonmark`.
Templates go in `src/main/resources/templates/` by default.

## JSON mapper configuration
Jackson is the default JSON mapper. Customize it:

```java
config.jsonMapper(new JavalinJackson().updateMapper(mapper -> {
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}));
```

Jackson auto-detects these modules if on classpath: `KotlinModule`, `JavaTimeModule`.
To use a different mapper (e.g. Gson), implement the `JsonMapper` interface.

## Lifecycle events
```java
config.events.serverStarting(() -> { /* starting up */ });
config.events.serverStarted(() -> { /* ready to serve */ });
config.events.serverStartFailed(() -> { /* failed to start */ });
config.events.serverStopping(() -> { /* shutting down */ });
config.events.serverStopped(() -> { /* stopped */ });
config.events.handlerAdded(info -> { /* route registered */ });
config.events.wsHandlerAdded(info -> { /* ws route registered */ });
```

## Single page application (SPA) support
Serve a single HTML file for all unmatched paths under a root (for Vue, React, etc.):

```java
config.spaRoot.addFile("/", "/public/index.html");
// or with dynamic handler
config.spaRoot.addHandler("/", ctx -> ctx.html("..."));
```

## JavalinVue
Server-side Vue.js integration — no build pipeline needed:

```java
config.registerPlugin(new JavalinVuePlugin());
config.routes.get("/my-page", new VueComponent("my-page"));
```

Vue files go in `src/main/resources/vue/components/`. Pass server state to Vue:
```java
config.registerPlugin(new JavalinVuePlugin(vue -> {
    vue.stateFunction = ctx -> Map.of("user", getUser(ctx));
}));
// Access in Vue template: {{ $javalin.state.user }}
```

## OpenAPI
API documentation is available via the `javalin-openapi` plugin (separate repository).
See [javalin.io/plugins/openapi](https://javalin.io/plugins/openapi) for setup, `@OpenApi` annotations, and Swagger UI integration.

## Testing with JavalinTest
Use `javalin-testtools` (included in `javalin-bundle`) for integration tests. `JavalinTest.test()` starts a real server and provides an HTTP client:

```java
import io.javalin.testtools.JavalinTest;
import static org.assertj.core.api.Assertions.assertThat;

Javalin app = Javalin.create(config -> {
    config.router.apiBuilder(() -> {
        get("/users", ctx -> ctx.json(userService.getAll()));
        post("/users", ctx -> {
            User user = ctx.bodyAsClass(User.class);
            userService.create(user);
            ctx.status(201);
        });
    });
});

@Test
public void GET_users_returns_200() {
    JavalinTest.test(app, (server, client) -> {
        assertThat(client.get("/users").code()).isEqualTo(200);
    });
}

@Test
public void POST_users_creates_user() {
    JavalinTest.test(app, (server, client) -> {
        var response = client.post("/users", new User("Alice"));
        assertThat(response.code()).isEqualTo(201);
    });
}
```

The `client` supports `get()`, `post()`, `put()`, `patch()`, `delete()` — all return an OkHttp `Response` with `.code()` and `.body().string()`. Each test gets a fresh server instance on a random port.

## Context methods quick reference
Request info:
- `ctx.body()` — request body as string
- `ctx.bodyAsClass(MyClass.class)` — deserialize JSON body
- `ctx.pathParam("id")` — path parameter (e.g., `/users/{id}`)
- `ctx.queryParam("name")` — query parameter (e.g., `?name=alice`)
- `ctx.formParam("field")` — form parameter
- `ctx.header("X-Custom")` — request header
- `ctx.cookie("name")` — cookie value
- `ctx.uploadedFile("file")` — single uploaded file
- `ctx.uploadedFiles("files")` — multiple uploaded files
- `ctx.attribute("key", value)` / `ctx.attribute("key")` — request-scoped attributes (share data between handlers)
- `ctx.sessionAttribute("key")` — session attribute
- `ctx.method()`, `ctx.url()`, `ctx.ip()`, `ctx.contentType()` — request metadata

Response:
- `ctx.result("text")` — set text response
- `ctx.json(myObject)` — serialize to JSON response
- `ctx.html("<h1>Hi</h1>")` — set HTML response
- `ctx.status(201)` — set status code
- `ctx.header("X-Custom", "value")` — set response header
- `ctx.cookie("name", "value")` — set cookie
- `ctx.redirect("/path")` — redirect
- `ctx.render("template.html", model)` — render template
- `ctx.contentType("application/json")` — set content type

## Important Javalin 7 changes (common pitfalls)
1. **Routes MUST be inside `config.routes`** — you cannot add routes after `.start()`. This is the biggest v7 change.
2. **`app.start()` no longer returns `this`** — chain off `Javalin.create()` instead of storing and calling start separately.
3. **Template rendering is modular** — add `javalin-rendering-{engine}` artifacts explicitly (e.g., `javalin-rendering-jte`).
4. **Jetty 12** — if configuring Jetty directly, use Jetty 12 APIs.
5. **Java 17+** is required.
# ╔══════════════════════════════════════════════════════════════╗
# ║          END OF JAVALIN FRAMEWORK INSTRUCTIONS               ║
# ╚══════════════════════════════════════════════════════════════╝

Add to your CLAUDE.md in your project root.

Cursor

---
description: Javalin 7.1.0 web framework conventions and API patterns
globs: "*.java,*.kt"
---
# ╔══════════════════════════════════════════════════════════════╗
# ║              JAVALIN FRAMEWORK INSTRUCTIONS                  ║
# ╚══════════════════════════════════════════════════════════════╝
# Javalin 7.1.0 — AI Coding Instructions

## Project overview
Javalin is a lightweight web framework for Java and Kotlin, built on Jetty 12.
- Requires Java 17+
- Maven: `io.javalin:javalin:7.1.0`
- Bundle (includes Jackson, Logback, testing): `io.javalin:javalin-bundle:7.1.0`
- SLF4J is the only required dependency — add Logback or another implementation for logging

## Core pattern
All configuration (routes, plugins, lifecycle) goes inside `Javalin.create(config -> { ... })`:

```java
// Java
import io.javalin.Javalin;

void main() {
    var app = Javalin.create(config -> {
        config.routes.get("/", ctx -> ctx.result("Hello World"));
    }).start(7070);
}
```

```kotlin
// Kotlin
import io.javalin.Javalin

fun main() {
    val app = Javalin.create { config ->
        config.routes.get("/") { ctx -> ctx.result("Hello World") }
    }.start(7070)
}
```

## Routing
Routes are defined via `config.routes`:

```java
config.routes.get("/users", ctx -> ctx.json(userDao.getAll()));
config.routes.get("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    ctx.json(userDao.getById(id));
});
config.routes.post("/users", ctx -> {
    User user = ctx.bodyAsClass(User.class);
    userDao.create(user);
    ctx.status(201);
});
config.routes.put("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    User user = ctx.bodyAsClass(User.class);
    userDao.update(id, user);
});
config.routes.delete("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    userDao.delete(id);
});
```

Supported methods: `get`, `post`, `put`, `patch`, `delete`, `query`, `head`, `options`.

Path parameters: `{param}` (no slashes) or `<param>` (allows slashes).
Wildcard: `/path/*` matches anything (but value cannot be extracted — use `<param>` instead).

## Handlers
The `Handler` interface is `ctx -> { ... }` with a void return. Set the response with:
- `ctx.result("text")` — plain text
- `ctx.json(object)` — JSON (requires Jackson or another JSON mapper)
- `ctx.html("<h1>Hi</h1>")` — HTML
- `ctx.status(code)` — HTTP status
- `ctx.redirect("/path")` — redirect
- `ctx.future(completableFuture)` — async

## Before/after handlers
```java
config.routes.before(ctx -> { /* runs before every request */ });
config.routes.after(ctx -> { /* runs after every request */ });
config.routes.beforeMatched(ctx -> { /* only if a route matched */ });
config.routes.afterMatched(ctx -> { /* only if a route matched */ });
config.routes.before("/api/*", ctx -> { /* path-scoped */ });
```

## Validation
```java
// Path parameter validation
int id = ctx.pathParamAsClass("id", Integer.class)
    .check(i -> i > 0, "ID must be positive")
    .get();

// Query parameter validation
int page = ctx.queryParamAsClass("page", Integer.class)
    .getOrDefault(1);

// Body validation
User user = ctx.bodyValidator(User.class)
    .check(u -> u.name != null, "Name required")
    .get();
```

## WebSockets
```java
config.routes.ws("/websocket", ws -> {
    ws.onConnect(ctx -> { /* WsConnectContext */ });
    ws.onMessage(ctx -> { ctx.send("Echo: " + ctx.message()); });
    ws.onClose(ctx -> { /* WsCloseContext */ });
    ws.onError(ctx -> { /* WsErrorContext */ });
});
```

## Server-Sent Events
```java
config.routes.sse("/sse", client -> {
    client.sendEvent("message", "Hello SSE");
    client.onClose(() -> { /* cleanup */ });
    client.keepAlive();
});
```

## Exception and error mapping
```java
config.error.exception(NotFoundException.class, (e, ctx) -> {
    ctx.status(404).result(e.getMessage());
});
config.error.error(404, ctx -> {
    ctx.result("Page not found");
});
```

## Access management
```java
config.accessManager((handler, ctx, routeRoles) -> {
    Role userRole = getUserRole(ctx);
    if (routeRoles.contains(userRole)) {
        handler.handle(ctx);
    } else {
        ctx.status(403).result("Forbidden");
    }
});
```

## Plugin configuration
```java
Javalin.create(config -> {
    // Bundled plugins
    config.bundledPlugins.enableCors(cors -> cors.addRule(it -> it.anyHost()));
    config.bundledPlugins.enableRouteOverview("/routes");
    config.bundledPlugins.enableDevLogging();

    // Static files
    config.staticFiles.add("/public", Location.CLASSPATH);
});
```

Available add-on artifacts: `javalin-rendering-{engine}` (JTE, Thymeleaf, Velocity, Pebble, Mustache, Handlebars), `javalin-micrometer`, `javalin-ssl`.

Custom plugins implement the `Plugin` interface.

## Handler groups
Use `apiBuilder` to group routes by path prefix (requires `import static io.javalin.apibuilder.ApiBuilder.*`):

```java
config.routes.apiBuilder(() -> {
    path("/users", () -> {
        get(UserController::getAllUsers);
        post(UserController::createUser);
        path("/{id}", () -> {
            get(UserController::getUser);
            patch(UserController::updateUser);
            delete(UserController::deleteUser);
        });
    });
});
```

CrudHandler shortcut — maps `getAll`, `getOne`, `create`, `update`, `delete` automatically:
```java
config.routes.apiBuilder(() -> {
    crud("users/{user-id}", new UserCrudHandler());
});
```

## Default HTTP responses
Throw typed exceptions for standard error responses (JSON body if client accepts JSON):
- `throw new BadRequestResponse("message")` — 400
- `throw new UnauthorizedResponse("message")` — 401
- `throw new ForbiddenResponse("message")` — 403
- `throw new NotFoundResponse("message")` — 404
- `throw new MethodNotAllowedResponse("message")` — 405
- `throw new ConflictResponse("message")` — 409
- `throw new GoneResponse("message")` — 410
- `throw new InternalServerErrorResponse("message")` — 500

All extend `HttpResponseException`. You can pass additional details: `new BadRequestResponse("msg", Map.of("detail", "value"))`.

## File uploads
```java
config.routes.post("/upload", ctx -> {
    UploadedFile file = ctx.uploadedFile("myFile");
    // file.filename(), file.content() (InputStream), file.size(), file.contentType()
    FileUtil.streamToFile(file.content(), "upload/" + file.filename());
});
// Multiple files
ctx.uploadedFiles("files"); // List<UploadedFile>
```

## Template rendering
Add a rendering engine artifact, e.g. `io.javalin:javalin-rendering-jte:7.1.0`:

```java
// Register the renderer
config.fileRenderer(new JavalinJte());

// Use in a handler
config.routes.get("/hello", ctx -> {
    ctx.render("hello.jte", Map.of("name", "World"));
});
```

Available engines: `javalin-rendering-jte`, `javalin-rendering-thymeleaf`, `javalin-rendering-velocity`, `javalin-rendering-pebble`, `javalin-rendering-mustache`, `javalin-rendering-handlebars`, `javalin-rendering-freemarker`, `javalin-rendering-commonmark`.
Templates go in `src/main/resources/templates/` by default.

## JSON mapper configuration
Jackson is the default JSON mapper. Customize it:

```java
config.jsonMapper(new JavalinJackson().updateMapper(mapper -> {
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}));
```

Jackson auto-detects these modules if on classpath: `KotlinModule`, `JavaTimeModule`.
To use a different mapper (e.g. Gson), implement the `JsonMapper` interface.

## Lifecycle events
```java
config.events.serverStarting(() -> { /* starting up */ });
config.events.serverStarted(() -> { /* ready to serve */ });
config.events.serverStartFailed(() -> { /* failed to start */ });
config.events.serverStopping(() -> { /* shutting down */ });
config.events.serverStopped(() -> { /* stopped */ });
config.events.handlerAdded(info -> { /* route registered */ });
config.events.wsHandlerAdded(info -> { /* ws route registered */ });
```

## Single page application (SPA) support
Serve a single HTML file for all unmatched paths under a root (for Vue, React, etc.):

```java
config.spaRoot.addFile("/", "/public/index.html");
// or with dynamic handler
config.spaRoot.addHandler("/", ctx -> ctx.html("..."));
```

## JavalinVue
Server-side Vue.js integration — no build pipeline needed:

```java
config.registerPlugin(new JavalinVuePlugin());
config.routes.get("/my-page", new VueComponent("my-page"));
```

Vue files go in `src/main/resources/vue/components/`. Pass server state to Vue:
```java
config.registerPlugin(new JavalinVuePlugin(vue -> {
    vue.stateFunction = ctx -> Map.of("user", getUser(ctx));
}));
// Access in Vue template: {{ $javalin.state.user }}
```

## OpenAPI
API documentation is available via the `javalin-openapi` plugin (separate repository).
See [javalin.io/plugins/openapi](https://javalin.io/plugins/openapi) for setup, `@OpenApi` annotations, and Swagger UI integration.

## Testing with JavalinTest
Use `javalin-testtools` (included in `javalin-bundle`) for integration tests. `JavalinTest.test()` starts a real server and provides an HTTP client:

```java
import io.javalin.testtools.JavalinTest;
import static org.assertj.core.api.Assertions.assertThat;

Javalin app = Javalin.create(config -> {
    config.router.apiBuilder(() -> {
        get("/users", ctx -> ctx.json(userService.getAll()));
        post("/users", ctx -> {
            User user = ctx.bodyAsClass(User.class);
            userService.create(user);
            ctx.status(201);
        });
    });
});

@Test
public void GET_users_returns_200() {
    JavalinTest.test(app, (server, client) -> {
        assertThat(client.get("/users").code()).isEqualTo(200);
    });
}

@Test
public void POST_users_creates_user() {
    JavalinTest.test(app, (server, client) -> {
        var response = client.post("/users", new User("Alice"));
        assertThat(response.code()).isEqualTo(201);
    });
}
```

The `client` supports `get()`, `post()`, `put()`, `patch()`, `delete()` — all return an OkHttp `Response` with `.code()` and `.body().string()`. Each test gets a fresh server instance on a random port.

## Context methods quick reference
Request info:
- `ctx.body()` — request body as string
- `ctx.bodyAsClass(MyClass.class)` — deserialize JSON body
- `ctx.pathParam("id")` — path parameter (e.g., `/users/{id}`)
- `ctx.queryParam("name")` — query parameter (e.g., `?name=alice`)
- `ctx.formParam("field")` — form parameter
- `ctx.header("X-Custom")` — request header
- `ctx.cookie("name")` — cookie value
- `ctx.uploadedFile("file")` — single uploaded file
- `ctx.uploadedFiles("files")` — multiple uploaded files
- `ctx.attribute("key", value)` / `ctx.attribute("key")` — request-scoped attributes (share data between handlers)
- `ctx.sessionAttribute("key")` — session attribute
- `ctx.method()`, `ctx.url()`, `ctx.ip()`, `ctx.contentType()` — request metadata

Response:
- `ctx.result("text")` — set text response
- `ctx.json(myObject)` — serialize to JSON response
- `ctx.html("<h1>Hi</h1>")` — set HTML response
- `ctx.status(201)` — set status code
- `ctx.header("X-Custom", "value")` — set response header
- `ctx.cookie("name", "value")` — set cookie
- `ctx.redirect("/path")` — redirect
- `ctx.render("template.html", model)` — render template
- `ctx.contentType("application/json")` — set content type

## Important Javalin 7 changes (common pitfalls)
1. **Routes MUST be inside `config.routes`** — you cannot add routes after `.start()`. This is the biggest v7 change.
2. **`app.start()` no longer returns `this`** — chain off `Javalin.create()` instead of storing and calling start separately.
3. **Template rendering is modular** — add `javalin-rendering-{engine}` artifacts explicitly (e.g., `javalin-rendering-jte`).
4. **Jetty 12** — if configuring Jetty directly, use Jetty 12 APIs.
5. **Java 17+** is required.
# ╔══════════════════════════════════════════════════════════════╗
# ║          END OF JAVALIN FRAMEWORK INSTRUCTIONS               ║
# ╚══════════════════════════════════════════════════════════════╝

Save as javalin.mdc in your .cursor/rules/ directory.

GitHub Copilot

# ╔══════════════════════════════════════════════════════════════╗
# ║              JAVALIN FRAMEWORK INSTRUCTIONS                  ║
# ╚══════════════════════════════════════════════════════════════╝
# Javalin 7.1.0 — AI Coding Instructions

## Project overview
Javalin is a lightweight web framework for Java and Kotlin, built on Jetty 12.
- Requires Java 17+
- Maven: `io.javalin:javalin:7.1.0`
- Bundle (includes Jackson, Logback, testing): `io.javalin:javalin-bundle:7.1.0`
- SLF4J is the only required dependency — add Logback or another implementation for logging

## Core pattern
All configuration (routes, plugins, lifecycle) goes inside `Javalin.create(config -> { ... })`:

```java
// Java
import io.javalin.Javalin;

void main() {
    var app = Javalin.create(config -> {
        config.routes.get("/", ctx -> ctx.result("Hello World"));
    }).start(7070);
}
```

```kotlin
// Kotlin
import io.javalin.Javalin

fun main() {
    val app = Javalin.create { config ->
        config.routes.get("/") { ctx -> ctx.result("Hello World") }
    }.start(7070)
}
```

## Routing
Routes are defined via `config.routes`:

```java
config.routes.get("/users", ctx -> ctx.json(userDao.getAll()));
config.routes.get("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    ctx.json(userDao.getById(id));
});
config.routes.post("/users", ctx -> {
    User user = ctx.bodyAsClass(User.class);
    userDao.create(user);
    ctx.status(201);
});
config.routes.put("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    User user = ctx.bodyAsClass(User.class);
    userDao.update(id, user);
});
config.routes.delete("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    userDao.delete(id);
});
```

Supported methods: `get`, `post`, `put`, `patch`, `delete`, `query`, `head`, `options`.

Path parameters: `{param}` (no slashes) or `<param>` (allows slashes).
Wildcard: `/path/*` matches anything (but value cannot be extracted — use `<param>` instead).

## Handlers
The `Handler` interface is `ctx -> { ... }` with a void return. Set the response with:
- `ctx.result("text")` — plain text
- `ctx.json(object)` — JSON (requires Jackson or another JSON mapper)
- `ctx.html("<h1>Hi</h1>")` — HTML
- `ctx.status(code)` — HTTP status
- `ctx.redirect("/path")` — redirect
- `ctx.future(completableFuture)` — async

## Before/after handlers
```java
config.routes.before(ctx -> { /* runs before every request */ });
config.routes.after(ctx -> { /* runs after every request */ });
config.routes.beforeMatched(ctx -> { /* only if a route matched */ });
config.routes.afterMatched(ctx -> { /* only if a route matched */ });
config.routes.before("/api/*", ctx -> { /* path-scoped */ });
```

## Validation
```java
// Path parameter validation
int id = ctx.pathParamAsClass("id", Integer.class)
    .check(i -> i > 0, "ID must be positive")
    .get();

// Query parameter validation
int page = ctx.queryParamAsClass("page", Integer.class)
    .getOrDefault(1);

// Body validation
User user = ctx.bodyValidator(User.class)
    .check(u -> u.name != null, "Name required")
    .get();
```

## WebSockets
```java
config.routes.ws("/websocket", ws -> {
    ws.onConnect(ctx -> { /* WsConnectContext */ });
    ws.onMessage(ctx -> { ctx.send("Echo: " + ctx.message()); });
    ws.onClose(ctx -> { /* WsCloseContext */ });
    ws.onError(ctx -> { /* WsErrorContext */ });
});
```

## Server-Sent Events
```java
config.routes.sse("/sse", client -> {
    client.sendEvent("message", "Hello SSE");
    client.onClose(() -> { /* cleanup */ });
    client.keepAlive();
});
```

## Exception and error mapping
```java
config.error.exception(NotFoundException.class, (e, ctx) -> {
    ctx.status(404).result(e.getMessage());
});
config.error.error(404, ctx -> {
    ctx.result("Page not found");
});
```

## Access management
```java
config.accessManager((handler, ctx, routeRoles) -> {
    Role userRole = getUserRole(ctx);
    if (routeRoles.contains(userRole)) {
        handler.handle(ctx);
    } else {
        ctx.status(403).result("Forbidden");
    }
});
```

## Plugin configuration
```java
Javalin.create(config -> {
    // Bundled plugins
    config.bundledPlugins.enableCors(cors -> cors.addRule(it -> it.anyHost()));
    config.bundledPlugins.enableRouteOverview("/routes");
    config.bundledPlugins.enableDevLogging();

    // Static files
    config.staticFiles.add("/public", Location.CLASSPATH);
});
```

Available add-on artifacts: `javalin-rendering-{engine}` (JTE, Thymeleaf, Velocity, Pebble, Mustache, Handlebars), `javalin-micrometer`, `javalin-ssl`.

Custom plugins implement the `Plugin` interface.

## Handler groups
Use `apiBuilder` to group routes by path prefix (requires `import static io.javalin.apibuilder.ApiBuilder.*`):

```java
config.routes.apiBuilder(() -> {
    path("/users", () -> {
        get(UserController::getAllUsers);
        post(UserController::createUser);
        path("/{id}", () -> {
            get(UserController::getUser);
            patch(UserController::updateUser);
            delete(UserController::deleteUser);
        });
    });
});
```

CrudHandler shortcut — maps `getAll`, `getOne`, `create`, `update`, `delete` automatically:
```java
config.routes.apiBuilder(() -> {
    crud("users/{user-id}", new UserCrudHandler());
});
```

## Default HTTP responses
Throw typed exceptions for standard error responses (JSON body if client accepts JSON):
- `throw new BadRequestResponse("message")` — 400
- `throw new UnauthorizedResponse("message")` — 401
- `throw new ForbiddenResponse("message")` — 403
- `throw new NotFoundResponse("message")` — 404
- `throw new MethodNotAllowedResponse("message")` — 405
- `throw new ConflictResponse("message")` — 409
- `throw new GoneResponse("message")` — 410
- `throw new InternalServerErrorResponse("message")` — 500

All extend `HttpResponseException`. You can pass additional details: `new BadRequestResponse("msg", Map.of("detail", "value"))`.

## File uploads
```java
config.routes.post("/upload", ctx -> {
    UploadedFile file = ctx.uploadedFile("myFile");
    // file.filename(), file.content() (InputStream), file.size(), file.contentType()
    FileUtil.streamToFile(file.content(), "upload/" + file.filename());
});
// Multiple files
ctx.uploadedFiles("files"); // List<UploadedFile>
```

## Template rendering
Add a rendering engine artifact, e.g. `io.javalin:javalin-rendering-jte:7.1.0`:

```java
// Register the renderer
config.fileRenderer(new JavalinJte());

// Use in a handler
config.routes.get("/hello", ctx -> {
    ctx.render("hello.jte", Map.of("name", "World"));
});
```

Available engines: `javalin-rendering-jte`, `javalin-rendering-thymeleaf`, `javalin-rendering-velocity`, `javalin-rendering-pebble`, `javalin-rendering-mustache`, `javalin-rendering-handlebars`, `javalin-rendering-freemarker`, `javalin-rendering-commonmark`.
Templates go in `src/main/resources/templates/` by default.

## JSON mapper configuration
Jackson is the default JSON mapper. Customize it:

```java
config.jsonMapper(new JavalinJackson().updateMapper(mapper -> {
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}));
```

Jackson auto-detects these modules if on classpath: `KotlinModule`, `JavaTimeModule`.
To use a different mapper (e.g. Gson), implement the `JsonMapper` interface.

## Lifecycle events
```java
config.events.serverStarting(() -> { /* starting up */ });
config.events.serverStarted(() -> { /* ready to serve */ });
config.events.serverStartFailed(() -> { /* failed to start */ });
config.events.serverStopping(() -> { /* shutting down */ });
config.events.serverStopped(() -> { /* stopped */ });
config.events.handlerAdded(info -> { /* route registered */ });
config.events.wsHandlerAdded(info -> { /* ws route registered */ });
```

## Single page application (SPA) support
Serve a single HTML file for all unmatched paths under a root (for Vue, React, etc.):

```java
config.spaRoot.addFile("/", "/public/index.html");
// or with dynamic handler
config.spaRoot.addHandler("/", ctx -> ctx.html("..."));
```

## JavalinVue
Server-side Vue.js integration — no build pipeline needed:

```java
config.registerPlugin(new JavalinVuePlugin());
config.routes.get("/my-page", new VueComponent("my-page"));
```

Vue files go in `src/main/resources/vue/components/`. Pass server state to Vue:
```java
config.registerPlugin(new JavalinVuePlugin(vue -> {
    vue.stateFunction = ctx -> Map.of("user", getUser(ctx));
}));
// Access in Vue template: {{ $javalin.state.user }}
```

## OpenAPI
API documentation is available via the `javalin-openapi` plugin (separate repository).
See [javalin.io/plugins/openapi](https://javalin.io/plugins/openapi) for setup, `@OpenApi` annotations, and Swagger UI integration.

## Testing with JavalinTest
Use `javalin-testtools` (included in `javalin-bundle`) for integration tests. `JavalinTest.test()` starts a real server and provides an HTTP client:

```java
import io.javalin.testtools.JavalinTest;
import static org.assertj.core.api.Assertions.assertThat;

Javalin app = Javalin.create(config -> {
    config.router.apiBuilder(() -> {
        get("/users", ctx -> ctx.json(userService.getAll()));
        post("/users", ctx -> {
            User user = ctx.bodyAsClass(User.class);
            userService.create(user);
            ctx.status(201);
        });
    });
});

@Test
public void GET_users_returns_200() {
    JavalinTest.test(app, (server, client) -> {
        assertThat(client.get("/users").code()).isEqualTo(200);
    });
}

@Test
public void POST_users_creates_user() {
    JavalinTest.test(app, (server, client) -> {
        var response = client.post("/users", new User("Alice"));
        assertThat(response.code()).isEqualTo(201);
    });
}
```

The `client` supports `get()`, `post()`, `put()`, `patch()`, `delete()` — all return an OkHttp `Response` with `.code()` and `.body().string()`. Each test gets a fresh server instance on a random port.

## Context methods quick reference
Request info:
- `ctx.body()` — request body as string
- `ctx.bodyAsClass(MyClass.class)` — deserialize JSON body
- `ctx.pathParam("id")` — path parameter (e.g., `/users/{id}`)
- `ctx.queryParam("name")` — query parameter (e.g., `?name=alice`)
- `ctx.formParam("field")` — form parameter
- `ctx.header("X-Custom")` — request header
- `ctx.cookie("name")` — cookie value
- `ctx.uploadedFile("file")` — single uploaded file
- `ctx.uploadedFiles("files")` — multiple uploaded files
- `ctx.attribute("key", value)` / `ctx.attribute("key")` — request-scoped attributes (share data between handlers)
- `ctx.sessionAttribute("key")` — session attribute
- `ctx.method()`, `ctx.url()`, `ctx.ip()`, `ctx.contentType()` — request metadata

Response:
- `ctx.result("text")` — set text response
- `ctx.json(myObject)` — serialize to JSON response
- `ctx.html("<h1>Hi</h1>")` — set HTML response
- `ctx.status(201)` — set status code
- `ctx.header("X-Custom", "value")` — set response header
- `ctx.cookie("name", "value")` — set cookie
- `ctx.redirect("/path")` — redirect
- `ctx.render("template.html", model)` — render template
- `ctx.contentType("application/json")` — set content type

## Important Javalin 7 changes (common pitfalls)
1. **Routes MUST be inside `config.routes`** — you cannot add routes after `.start()`. This is the biggest v7 change.
2. **`app.start()` no longer returns `this`** — chain off `Javalin.create()` instead of storing and calling start separately.
3. **Template rendering is modular** — add `javalin-rendering-{engine}` artifacts explicitly (e.g., `javalin-rendering-jte`).
4. **Jetty 12** — if configuring Jetty directly, use Jetty 12 APIs.
5. **Java 17+** is required.
# ╔══════════════════════════════════════════════════════════════╗
# ║          END OF JAVALIN FRAMEWORK INSTRUCTIONS               ║
# ╚══════════════════════════════════════════════════════════════╝

Add to your .github/copilot-instructions.md.

Windsurf

# ╔══════════════════════════════════════════════════════════════╗
# ║              JAVALIN FRAMEWORK INSTRUCTIONS                  ║
# ╚══════════════════════════════════════════════════════════════╝
# Javalin 7.1.0 — AI Coding Instructions

## Project overview
Javalin is a lightweight web framework for Java and Kotlin, built on Jetty 12.
- Requires Java 17+
- Maven: `io.javalin:javalin:7.1.0`
- Bundle (includes Jackson, Logback, testing): `io.javalin:javalin-bundle:7.1.0`
- SLF4J is the only required dependency — add Logback or another implementation for logging

## Core pattern
All configuration (routes, plugins, lifecycle) goes inside `Javalin.create(config -> { ... })`:

```java
// Java
import io.javalin.Javalin;

void main() {
    var app = Javalin.create(config -> {
        config.routes.get("/", ctx -> ctx.result("Hello World"));
    }).start(7070);
}
```

```kotlin
// Kotlin
import io.javalin.Javalin

fun main() {
    val app = Javalin.create { config ->
        config.routes.get("/") { ctx -> ctx.result("Hello World") }
    }.start(7070)
}
```

## Routing
Routes are defined via `config.routes`:

```java
config.routes.get("/users", ctx -> ctx.json(userDao.getAll()));
config.routes.get("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    ctx.json(userDao.getById(id));
});
config.routes.post("/users", ctx -> {
    User user = ctx.bodyAsClass(User.class);
    userDao.create(user);
    ctx.status(201);
});
config.routes.put("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    User user = ctx.bodyAsClass(User.class);
    userDao.update(id, user);
});
config.routes.delete("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    userDao.delete(id);
});
```

Supported methods: `get`, `post`, `put`, `patch`, `delete`, `query`, `head`, `options`.

Path parameters: `{param}` (no slashes) or `<param>` (allows slashes).
Wildcard: `/path/*` matches anything (but value cannot be extracted — use `<param>` instead).

## Handlers
The `Handler` interface is `ctx -> { ... }` with a void return. Set the response with:
- `ctx.result("text")` — plain text
- `ctx.json(object)` — JSON (requires Jackson or another JSON mapper)
- `ctx.html("<h1>Hi</h1>")` — HTML
- `ctx.status(code)` — HTTP status
- `ctx.redirect("/path")` — redirect
- `ctx.future(completableFuture)` — async

## Before/after handlers
```java
config.routes.before(ctx -> { /* runs before every request */ });
config.routes.after(ctx -> { /* runs after every request */ });
config.routes.beforeMatched(ctx -> { /* only if a route matched */ });
config.routes.afterMatched(ctx -> { /* only if a route matched */ });
config.routes.before("/api/*", ctx -> { /* path-scoped */ });
```

## Validation
```java
// Path parameter validation
int id = ctx.pathParamAsClass("id", Integer.class)
    .check(i -> i > 0, "ID must be positive")
    .get();

// Query parameter validation
int page = ctx.queryParamAsClass("page", Integer.class)
    .getOrDefault(1);

// Body validation
User user = ctx.bodyValidator(User.class)
    .check(u -> u.name != null, "Name required")
    .get();
```

## WebSockets
```java
config.routes.ws("/websocket", ws -> {
    ws.onConnect(ctx -> { /* WsConnectContext */ });
    ws.onMessage(ctx -> { ctx.send("Echo: " + ctx.message()); });
    ws.onClose(ctx -> { /* WsCloseContext */ });
    ws.onError(ctx -> { /* WsErrorContext */ });
});
```

## Server-Sent Events
```java
config.routes.sse("/sse", client -> {
    client.sendEvent("message", "Hello SSE");
    client.onClose(() -> { /* cleanup */ });
    client.keepAlive();
});
```

## Exception and error mapping
```java
config.error.exception(NotFoundException.class, (e, ctx) -> {
    ctx.status(404).result(e.getMessage());
});
config.error.error(404, ctx -> {
    ctx.result("Page not found");
});
```

## Access management
```java
config.accessManager((handler, ctx, routeRoles) -> {
    Role userRole = getUserRole(ctx);
    if (routeRoles.contains(userRole)) {
        handler.handle(ctx);
    } else {
        ctx.status(403).result("Forbidden");
    }
});
```

## Plugin configuration
```java
Javalin.create(config -> {
    // Bundled plugins
    config.bundledPlugins.enableCors(cors -> cors.addRule(it -> it.anyHost()));
    config.bundledPlugins.enableRouteOverview("/routes");
    config.bundledPlugins.enableDevLogging();

    // Static files
    config.staticFiles.add("/public", Location.CLASSPATH);
});
```

Available add-on artifacts: `javalin-rendering-{engine}` (JTE, Thymeleaf, Velocity, Pebble, Mustache, Handlebars), `javalin-micrometer`, `javalin-ssl`.

Custom plugins implement the `Plugin` interface.

## Handler groups
Use `apiBuilder` to group routes by path prefix (requires `import static io.javalin.apibuilder.ApiBuilder.*`):

```java
config.routes.apiBuilder(() -> {
    path("/users", () -> {
        get(UserController::getAllUsers);
        post(UserController::createUser);
        path("/{id}", () -> {
            get(UserController::getUser);
            patch(UserController::updateUser);
            delete(UserController::deleteUser);
        });
    });
});
```

CrudHandler shortcut — maps `getAll`, `getOne`, `create`, `update`, `delete` automatically:
```java
config.routes.apiBuilder(() -> {
    crud("users/{user-id}", new UserCrudHandler());
});
```

## Default HTTP responses
Throw typed exceptions for standard error responses (JSON body if client accepts JSON):
- `throw new BadRequestResponse("message")` — 400
- `throw new UnauthorizedResponse("message")` — 401
- `throw new ForbiddenResponse("message")` — 403
- `throw new NotFoundResponse("message")` — 404
- `throw new MethodNotAllowedResponse("message")` — 405
- `throw new ConflictResponse("message")` — 409
- `throw new GoneResponse("message")` — 410
- `throw new InternalServerErrorResponse("message")` — 500

All extend `HttpResponseException`. You can pass additional details: `new BadRequestResponse("msg", Map.of("detail", "value"))`.

## File uploads
```java
config.routes.post("/upload", ctx -> {
    UploadedFile file = ctx.uploadedFile("myFile");
    // file.filename(), file.content() (InputStream), file.size(), file.contentType()
    FileUtil.streamToFile(file.content(), "upload/" + file.filename());
});
// Multiple files
ctx.uploadedFiles("files"); // List<UploadedFile>
```

## Template rendering
Add a rendering engine artifact, e.g. `io.javalin:javalin-rendering-jte:7.1.0`:

```java
// Register the renderer
config.fileRenderer(new JavalinJte());

// Use in a handler
config.routes.get("/hello", ctx -> {
    ctx.render("hello.jte", Map.of("name", "World"));
});
```

Available engines: `javalin-rendering-jte`, `javalin-rendering-thymeleaf`, `javalin-rendering-velocity`, `javalin-rendering-pebble`, `javalin-rendering-mustache`, `javalin-rendering-handlebars`, `javalin-rendering-freemarker`, `javalin-rendering-commonmark`.
Templates go in `src/main/resources/templates/` by default.

## JSON mapper configuration
Jackson is the default JSON mapper. Customize it:

```java
config.jsonMapper(new JavalinJackson().updateMapper(mapper -> {
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}));
```

Jackson auto-detects these modules if on classpath: `KotlinModule`, `JavaTimeModule`.
To use a different mapper (e.g. Gson), implement the `JsonMapper` interface.

## Lifecycle events
```java
config.events.serverStarting(() -> { /* starting up */ });
config.events.serverStarted(() -> { /* ready to serve */ });
config.events.serverStartFailed(() -> { /* failed to start */ });
config.events.serverStopping(() -> { /* shutting down */ });
config.events.serverStopped(() -> { /* stopped */ });
config.events.handlerAdded(info -> { /* route registered */ });
config.events.wsHandlerAdded(info -> { /* ws route registered */ });
```

## Single page application (SPA) support
Serve a single HTML file for all unmatched paths under a root (for Vue, React, etc.):

```java
config.spaRoot.addFile("/", "/public/index.html");
// or with dynamic handler
config.spaRoot.addHandler("/", ctx -> ctx.html("..."));
```

## JavalinVue
Server-side Vue.js integration — no build pipeline needed:

```java
config.registerPlugin(new JavalinVuePlugin());
config.routes.get("/my-page", new VueComponent("my-page"));
```

Vue files go in `src/main/resources/vue/components/`. Pass server state to Vue:
```java
config.registerPlugin(new JavalinVuePlugin(vue -> {
    vue.stateFunction = ctx -> Map.of("user", getUser(ctx));
}));
// Access in Vue template: {{ $javalin.state.user }}
```

## OpenAPI
API documentation is available via the `javalin-openapi` plugin (separate repository).
See [javalin.io/plugins/openapi](https://javalin.io/plugins/openapi) for setup, `@OpenApi` annotations, and Swagger UI integration.

## Testing with JavalinTest
Use `javalin-testtools` (included in `javalin-bundle`) for integration tests. `JavalinTest.test()` starts a real server and provides an HTTP client:

```java
import io.javalin.testtools.JavalinTest;
import static org.assertj.core.api.Assertions.assertThat;

Javalin app = Javalin.create(config -> {
    config.router.apiBuilder(() -> {
        get("/users", ctx -> ctx.json(userService.getAll()));
        post("/users", ctx -> {
            User user = ctx.bodyAsClass(User.class);
            userService.create(user);
            ctx.status(201);
        });
    });
});

@Test
public void GET_users_returns_200() {
    JavalinTest.test(app, (server, client) -> {
        assertThat(client.get("/users").code()).isEqualTo(200);
    });
}

@Test
public void POST_users_creates_user() {
    JavalinTest.test(app, (server, client) -> {
        var response = client.post("/users", new User("Alice"));
        assertThat(response.code()).isEqualTo(201);
    });
}
```

The `client` supports `get()`, `post()`, `put()`, `patch()`, `delete()` — all return an OkHttp `Response` with `.code()` and `.body().string()`. Each test gets a fresh server instance on a random port.

## Context methods quick reference
Request info:
- `ctx.body()` — request body as string
- `ctx.bodyAsClass(MyClass.class)` — deserialize JSON body
- `ctx.pathParam("id")` — path parameter (e.g., `/users/{id}`)
- `ctx.queryParam("name")` — query parameter (e.g., `?name=alice`)
- `ctx.formParam("field")` — form parameter
- `ctx.header("X-Custom")` — request header
- `ctx.cookie("name")` — cookie value
- `ctx.uploadedFile("file")` — single uploaded file
- `ctx.uploadedFiles("files")` — multiple uploaded files
- `ctx.attribute("key", value)` / `ctx.attribute("key")` — request-scoped attributes (share data between handlers)
- `ctx.sessionAttribute("key")` — session attribute
- `ctx.method()`, `ctx.url()`, `ctx.ip()`, `ctx.contentType()` — request metadata

Response:
- `ctx.result("text")` — set text response
- `ctx.json(myObject)` — serialize to JSON response
- `ctx.html("<h1>Hi</h1>")` — set HTML response
- `ctx.status(201)` — set status code
- `ctx.header("X-Custom", "value")` — set response header
- `ctx.cookie("name", "value")` — set cookie
- `ctx.redirect("/path")` — redirect
- `ctx.render("template.html", model)` — render template
- `ctx.contentType("application/json")` — set content type

## Important Javalin 7 changes (common pitfalls)
1. **Routes MUST be inside `config.routes`** — you cannot add routes after `.start()`. This is the biggest v7 change.
2. **`app.start()` no longer returns `this`** — chain off `Javalin.create()` instead of storing and calling start separately.
3. **Template rendering is modular** — add `javalin-rendering-{engine}` artifacts explicitly (e.g., `javalin-rendering-jte`).
4. **Jetty 12** — if configuring Jetty directly, use Jetty 12 APIs.
5. **Java 17+** is required.
# ╔══════════════════════════════════════════════════════════════╗
# ║          END OF JAVALIN FRAMEWORK INSTRUCTIONS               ║
# ╚══════════════════════════════════════════════════════════════╝

Add to your .windsurfrules in your project root.

Codex / AGENTS.md

# ╔══════════════════════════════════════════════════════════════╗
# ║              JAVALIN FRAMEWORK INSTRUCTIONS                  ║
# ╚══════════════════════════════════════════════════════════════╝
# Javalin 7.1.0 — AI Coding Instructions

## Project overview
Javalin is a lightweight web framework for Java and Kotlin, built on Jetty 12.
- Requires Java 17+
- Maven: `io.javalin:javalin:7.1.0`
- Bundle (includes Jackson, Logback, testing): `io.javalin:javalin-bundle:7.1.0`
- SLF4J is the only required dependency — add Logback or another implementation for logging

## Core pattern
All configuration (routes, plugins, lifecycle) goes inside `Javalin.create(config -> { ... })`:

```java
// Java
import io.javalin.Javalin;

void main() {
    var app = Javalin.create(config -> {
        config.routes.get("/", ctx -> ctx.result("Hello World"));
    }).start(7070);
}
```

```kotlin
// Kotlin
import io.javalin.Javalin

fun main() {
    val app = Javalin.create { config ->
        config.routes.get("/") { ctx -> ctx.result("Hello World") }
    }.start(7070)
}
```

## Routing
Routes are defined via `config.routes`:

```java
config.routes.get("/users", ctx -> ctx.json(userDao.getAll()));
config.routes.get("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    ctx.json(userDao.getById(id));
});
config.routes.post("/users", ctx -> {
    User user = ctx.bodyAsClass(User.class);
    userDao.create(user);
    ctx.status(201);
});
config.routes.put("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    User user = ctx.bodyAsClass(User.class);
    userDao.update(id, user);
});
config.routes.delete("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    userDao.delete(id);
});
```

Supported methods: `get`, `post`, `put`, `patch`, `delete`, `query`, `head`, `options`.

Path parameters: `{param}` (no slashes) or `<param>` (allows slashes).
Wildcard: `/path/*` matches anything (but value cannot be extracted — use `<param>` instead).

## Handlers
The `Handler` interface is `ctx -> { ... }` with a void return. Set the response with:
- `ctx.result("text")` — plain text
- `ctx.json(object)` — JSON (requires Jackson or another JSON mapper)
- `ctx.html("<h1>Hi</h1>")` — HTML
- `ctx.status(code)` — HTTP status
- `ctx.redirect("/path")` — redirect
- `ctx.future(completableFuture)` — async

## Before/after handlers
```java
config.routes.before(ctx -> { /* runs before every request */ });
config.routes.after(ctx -> { /* runs after every request */ });
config.routes.beforeMatched(ctx -> { /* only if a route matched */ });
config.routes.afterMatched(ctx -> { /* only if a route matched */ });
config.routes.before("/api/*", ctx -> { /* path-scoped */ });
```

## Validation
```java
// Path parameter validation
int id = ctx.pathParamAsClass("id", Integer.class)
    .check(i -> i > 0, "ID must be positive")
    .get();

// Query parameter validation
int page = ctx.queryParamAsClass("page", Integer.class)
    .getOrDefault(1);

// Body validation
User user = ctx.bodyValidator(User.class)
    .check(u -> u.name != null, "Name required")
    .get();
```

## WebSockets
```java
config.routes.ws("/websocket", ws -> {
    ws.onConnect(ctx -> { /* WsConnectContext */ });
    ws.onMessage(ctx -> { ctx.send("Echo: " + ctx.message()); });
    ws.onClose(ctx -> { /* WsCloseContext */ });
    ws.onError(ctx -> { /* WsErrorContext */ });
});
```

## Server-Sent Events
```java
config.routes.sse("/sse", client -> {
    client.sendEvent("message", "Hello SSE");
    client.onClose(() -> { /* cleanup */ });
    client.keepAlive();
});
```

## Exception and error mapping
```java
config.error.exception(NotFoundException.class, (e, ctx) -> {
    ctx.status(404).result(e.getMessage());
});
config.error.error(404, ctx -> {
    ctx.result("Page not found");
});
```

## Access management
```java
config.accessManager((handler, ctx, routeRoles) -> {
    Role userRole = getUserRole(ctx);
    if (routeRoles.contains(userRole)) {
        handler.handle(ctx);
    } else {
        ctx.status(403).result("Forbidden");
    }
});
```

## Plugin configuration
```java
Javalin.create(config -> {
    // Bundled plugins
    config.bundledPlugins.enableCors(cors -> cors.addRule(it -> it.anyHost()));
    config.bundledPlugins.enableRouteOverview("/routes");
    config.bundledPlugins.enableDevLogging();

    // Static files
    config.staticFiles.add("/public", Location.CLASSPATH);
});
```

Available add-on artifacts: `javalin-rendering-{engine}` (JTE, Thymeleaf, Velocity, Pebble, Mustache, Handlebars), `javalin-micrometer`, `javalin-ssl`.

Custom plugins implement the `Plugin` interface.

## Handler groups
Use `apiBuilder` to group routes by path prefix (requires `import static io.javalin.apibuilder.ApiBuilder.*`):

```java
config.routes.apiBuilder(() -> {
    path("/users", () -> {
        get(UserController::getAllUsers);
        post(UserController::createUser);
        path("/{id}", () -> {
            get(UserController::getUser);
            patch(UserController::updateUser);
            delete(UserController::deleteUser);
        });
    });
});
```

CrudHandler shortcut — maps `getAll`, `getOne`, `create`, `update`, `delete` automatically:
```java
config.routes.apiBuilder(() -> {
    crud("users/{user-id}", new UserCrudHandler());
});
```

## Default HTTP responses
Throw typed exceptions for standard error responses (JSON body if client accepts JSON):
- `throw new BadRequestResponse("message")` — 400
- `throw new UnauthorizedResponse("message")` — 401
- `throw new ForbiddenResponse("message")` — 403
- `throw new NotFoundResponse("message")` — 404
- `throw new MethodNotAllowedResponse("message")` — 405
- `throw new ConflictResponse("message")` — 409
- `throw new GoneResponse("message")` — 410
- `throw new InternalServerErrorResponse("message")` — 500

All extend `HttpResponseException`. You can pass additional details: `new BadRequestResponse("msg", Map.of("detail", "value"))`.

## File uploads
```java
config.routes.post("/upload", ctx -> {
    UploadedFile file = ctx.uploadedFile("myFile");
    // file.filename(), file.content() (InputStream), file.size(), file.contentType()
    FileUtil.streamToFile(file.content(), "upload/" + file.filename());
});
// Multiple files
ctx.uploadedFiles("files"); // List<UploadedFile>
```

## Template rendering
Add a rendering engine artifact, e.g. `io.javalin:javalin-rendering-jte:7.1.0`:

```java
// Register the renderer
config.fileRenderer(new JavalinJte());

// Use in a handler
config.routes.get("/hello", ctx -> {
    ctx.render("hello.jte", Map.of("name", "World"));
});
```

Available engines: `javalin-rendering-jte`, `javalin-rendering-thymeleaf`, `javalin-rendering-velocity`, `javalin-rendering-pebble`, `javalin-rendering-mustache`, `javalin-rendering-handlebars`, `javalin-rendering-freemarker`, `javalin-rendering-commonmark`.
Templates go in `src/main/resources/templates/` by default.

## JSON mapper configuration
Jackson is the default JSON mapper. Customize it:

```java
config.jsonMapper(new JavalinJackson().updateMapper(mapper -> {
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}));
```

Jackson auto-detects these modules if on classpath: `KotlinModule`, `JavaTimeModule`.
To use a different mapper (e.g. Gson), implement the `JsonMapper` interface.

## Lifecycle events
```java
config.events.serverStarting(() -> { /* starting up */ });
config.events.serverStarted(() -> { /* ready to serve */ });
config.events.serverStartFailed(() -> { /* failed to start */ });
config.events.serverStopping(() -> { /* shutting down */ });
config.events.serverStopped(() -> { /* stopped */ });
config.events.handlerAdded(info -> { /* route registered */ });
config.events.wsHandlerAdded(info -> { /* ws route registered */ });
```

## Single page application (SPA) support
Serve a single HTML file for all unmatched paths under a root (for Vue, React, etc.):

```java
config.spaRoot.addFile("/", "/public/index.html");
// or with dynamic handler
config.spaRoot.addHandler("/", ctx -> ctx.html("..."));
```

## JavalinVue
Server-side Vue.js integration — no build pipeline needed:

```java
config.registerPlugin(new JavalinVuePlugin());
config.routes.get("/my-page", new VueComponent("my-page"));
```

Vue files go in `src/main/resources/vue/components/`. Pass server state to Vue:
```java
config.registerPlugin(new JavalinVuePlugin(vue -> {
    vue.stateFunction = ctx -> Map.of("user", getUser(ctx));
}));
// Access in Vue template: {{ $javalin.state.user }}
```

## OpenAPI
API documentation is available via the `javalin-openapi` plugin (separate repository).
See [javalin.io/plugins/openapi](https://javalin.io/plugins/openapi) for setup, `@OpenApi` annotations, and Swagger UI integration.

## Testing with JavalinTest
Use `javalin-testtools` (included in `javalin-bundle`) for integration tests. `JavalinTest.test()` starts a real server and provides an HTTP client:

```java
import io.javalin.testtools.JavalinTest;
import static org.assertj.core.api.Assertions.assertThat;

Javalin app = Javalin.create(config -> {
    config.router.apiBuilder(() -> {
        get("/users", ctx -> ctx.json(userService.getAll()));
        post("/users", ctx -> {
            User user = ctx.bodyAsClass(User.class);
            userService.create(user);
            ctx.status(201);
        });
    });
});

@Test
public void GET_users_returns_200() {
    JavalinTest.test(app, (server, client) -> {
        assertThat(client.get("/users").code()).isEqualTo(200);
    });
}

@Test
public void POST_users_creates_user() {
    JavalinTest.test(app, (server, client) -> {
        var response = client.post("/users", new User("Alice"));
        assertThat(response.code()).isEqualTo(201);
    });
}
```

The `client` supports `get()`, `post()`, `put()`, `patch()`, `delete()` — all return an OkHttp `Response` with `.code()` and `.body().string()`. Each test gets a fresh server instance on a random port.

## Context methods quick reference
Request info:
- `ctx.body()` — request body as string
- `ctx.bodyAsClass(MyClass.class)` — deserialize JSON body
- `ctx.pathParam("id")` — path parameter (e.g., `/users/{id}`)
- `ctx.queryParam("name")` — query parameter (e.g., `?name=alice`)
- `ctx.formParam("field")` — form parameter
- `ctx.header("X-Custom")` — request header
- `ctx.cookie("name")` — cookie value
- `ctx.uploadedFile("file")` — single uploaded file
- `ctx.uploadedFiles("files")` — multiple uploaded files
- `ctx.attribute("key", value)` / `ctx.attribute("key")` — request-scoped attributes (share data between handlers)
- `ctx.sessionAttribute("key")` — session attribute
- `ctx.method()`, `ctx.url()`, `ctx.ip()`, `ctx.contentType()` — request metadata

Response:
- `ctx.result("text")` — set text response
- `ctx.json(myObject)` — serialize to JSON response
- `ctx.html("<h1>Hi</h1>")` — set HTML response
- `ctx.status(201)` — set status code
- `ctx.header("X-Custom", "value")` — set response header
- `ctx.cookie("name", "value")` — set cookie
- `ctx.redirect("/path")` — redirect
- `ctx.render("template.html", model)` — render template
- `ctx.contentType("application/json")` — set content type

## Important Javalin 7 changes (common pitfalls)
1. **Routes MUST be inside `config.routes`** — you cannot add routes after `.start()`. This is the biggest v7 change.
2. **`app.start()` no longer returns `this`** — chain off `Javalin.create()` instead of storing and calling start separately.
3. **Template rendering is modular** — add `javalin-rendering-{engine}` artifacts explicitly (e.g., `javalin-rendering-jte`).
4. **Jetty 12** — if configuring Jetty directly, use Jetty 12 APIs.
5. **Java 17+** is required.
# ╔══════════════════════════════════════════════════════════════╗
# ║          END OF JAVALIN FRAMEWORK INSTRUCTIONS               ║
# ╚══════════════════════════════════════════════════════════════╝

Add to your AGENTS.md in your project root.

Gemini

# ╔══════════════════════════════════════════════════════════════╗
# ║              JAVALIN FRAMEWORK INSTRUCTIONS                  ║
# ╚══════════════════════════════════════════════════════════════╝
# Javalin 7.1.0 — AI Coding Instructions

## Project overview
Javalin is a lightweight web framework for Java and Kotlin, built on Jetty 12.
- Requires Java 17+
- Maven: `io.javalin:javalin:7.1.0`
- Bundle (includes Jackson, Logback, testing): `io.javalin:javalin-bundle:7.1.0`
- SLF4J is the only required dependency — add Logback or another implementation for logging

## Core pattern
All configuration (routes, plugins, lifecycle) goes inside `Javalin.create(config -> { ... })`:

```java
// Java
import io.javalin.Javalin;

void main() {
    var app = Javalin.create(config -> {
        config.routes.get("/", ctx -> ctx.result("Hello World"));
    }).start(7070);
}
```

```kotlin
// Kotlin
import io.javalin.Javalin

fun main() {
    val app = Javalin.create { config ->
        config.routes.get("/") { ctx -> ctx.result("Hello World") }
    }.start(7070)
}
```

## Routing
Routes are defined via `config.routes`:

```java
config.routes.get("/users", ctx -> ctx.json(userDao.getAll()));
config.routes.get("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    ctx.json(userDao.getById(id));
});
config.routes.post("/users", ctx -> {
    User user = ctx.bodyAsClass(User.class);
    userDao.create(user);
    ctx.status(201);
});
config.routes.put("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    User user = ctx.bodyAsClass(User.class);
    userDao.update(id, user);
});
config.routes.delete("/users/{id}", ctx -> {
    int id = ctx.pathParamAsClass("id", Integer.class).get();
    userDao.delete(id);
});
```

Supported methods: `get`, `post`, `put`, `patch`, `delete`, `query`, `head`, `options`.

Path parameters: `{param}` (no slashes) or `<param>` (allows slashes).
Wildcard: `/path/*` matches anything (but value cannot be extracted — use `<param>` instead).

## Handlers
The `Handler` interface is `ctx -> { ... }` with a void return. Set the response with:
- `ctx.result("text")` — plain text
- `ctx.json(object)` — JSON (requires Jackson or another JSON mapper)
- `ctx.html("<h1>Hi</h1>")` — HTML
- `ctx.status(code)` — HTTP status
- `ctx.redirect("/path")` — redirect
- `ctx.future(completableFuture)` — async

## Before/after handlers
```java
config.routes.before(ctx -> { /* runs before every request */ });
config.routes.after(ctx -> { /* runs after every request */ });
config.routes.beforeMatched(ctx -> { /* only if a route matched */ });
config.routes.afterMatched(ctx -> { /* only if a route matched */ });
config.routes.before("/api/*", ctx -> { /* path-scoped */ });
```

## Validation
```java
// Path parameter validation
int id = ctx.pathParamAsClass("id", Integer.class)
    .check(i -> i > 0, "ID must be positive")
    .get();

// Query parameter validation
int page = ctx.queryParamAsClass("page", Integer.class)
    .getOrDefault(1);

// Body validation
User user = ctx.bodyValidator(User.class)
    .check(u -> u.name != null, "Name required")
    .get();
```

## WebSockets
```java
config.routes.ws("/websocket", ws -> {
    ws.onConnect(ctx -> { /* WsConnectContext */ });
    ws.onMessage(ctx -> { ctx.send("Echo: " + ctx.message()); });
    ws.onClose(ctx -> { /* WsCloseContext */ });
    ws.onError(ctx -> { /* WsErrorContext */ });
});
```

## Server-Sent Events
```java
config.routes.sse("/sse", client -> {
    client.sendEvent("message", "Hello SSE");
    client.onClose(() -> { /* cleanup */ });
    client.keepAlive();
});
```

## Exception and error mapping
```java
config.error.exception(NotFoundException.class, (e, ctx) -> {
    ctx.status(404).result(e.getMessage());
});
config.error.error(404, ctx -> {
    ctx.result("Page not found");
});
```

## Access management
```java
config.accessManager((handler, ctx, routeRoles) -> {
    Role userRole = getUserRole(ctx);
    if (routeRoles.contains(userRole)) {
        handler.handle(ctx);
    } else {
        ctx.status(403).result("Forbidden");
    }
});
```

## Plugin configuration
```java
Javalin.create(config -> {
    // Bundled plugins
    config.bundledPlugins.enableCors(cors -> cors.addRule(it -> it.anyHost()));
    config.bundledPlugins.enableRouteOverview("/routes");
    config.bundledPlugins.enableDevLogging();

    // Static files
    config.staticFiles.add("/public", Location.CLASSPATH);
});
```

Available add-on artifacts: `javalin-rendering-{engine}` (JTE, Thymeleaf, Velocity, Pebble, Mustache, Handlebars), `javalin-micrometer`, `javalin-ssl`.

Custom plugins implement the `Plugin` interface.

## Handler groups
Use `apiBuilder` to group routes by path prefix (requires `import static io.javalin.apibuilder.ApiBuilder.*`):

```java
config.routes.apiBuilder(() -> {
    path("/users", () -> {
        get(UserController::getAllUsers);
        post(UserController::createUser);
        path("/{id}", () -> {
            get(UserController::getUser);
            patch(UserController::updateUser);
            delete(UserController::deleteUser);
        });
    });
});
```

CrudHandler shortcut — maps `getAll`, `getOne`, `create`, `update`, `delete` automatically:
```java
config.routes.apiBuilder(() -> {
    crud("users/{user-id}", new UserCrudHandler());
});
```

## Default HTTP responses
Throw typed exceptions for standard error responses (JSON body if client accepts JSON):
- `throw new BadRequestResponse("message")` — 400
- `throw new UnauthorizedResponse("message")` — 401
- `throw new ForbiddenResponse("message")` — 403
- `throw new NotFoundResponse("message")` — 404
- `throw new MethodNotAllowedResponse("message")` — 405
- `throw new ConflictResponse("message")` — 409
- `throw new GoneResponse("message")` — 410
- `throw new InternalServerErrorResponse("message")` — 500

All extend `HttpResponseException`. You can pass additional details: `new BadRequestResponse("msg", Map.of("detail", "value"))`.

## File uploads
```java
config.routes.post("/upload", ctx -> {
    UploadedFile file = ctx.uploadedFile("myFile");
    // file.filename(), file.content() (InputStream), file.size(), file.contentType()
    FileUtil.streamToFile(file.content(), "upload/" + file.filename());
});
// Multiple files
ctx.uploadedFiles("files"); // List<UploadedFile>
```

## Template rendering
Add a rendering engine artifact, e.g. `io.javalin:javalin-rendering-jte:7.1.0`:

```java
// Register the renderer
config.fileRenderer(new JavalinJte());

// Use in a handler
config.routes.get("/hello", ctx -> {
    ctx.render("hello.jte", Map.of("name", "World"));
});
```

Available engines: `javalin-rendering-jte`, `javalin-rendering-thymeleaf`, `javalin-rendering-velocity`, `javalin-rendering-pebble`, `javalin-rendering-mustache`, `javalin-rendering-handlebars`, `javalin-rendering-freemarker`, `javalin-rendering-commonmark`.
Templates go in `src/main/resources/templates/` by default.

## JSON mapper configuration
Jackson is the default JSON mapper. Customize it:

```java
config.jsonMapper(new JavalinJackson().updateMapper(mapper -> {
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}));
```

Jackson auto-detects these modules if on classpath: `KotlinModule`, `JavaTimeModule`.
To use a different mapper (e.g. Gson), implement the `JsonMapper` interface.

## Lifecycle events
```java
config.events.serverStarting(() -> { /* starting up */ });
config.events.serverStarted(() -> { /* ready to serve */ });
config.events.serverStartFailed(() -> { /* failed to start */ });
config.events.serverStopping(() -> { /* shutting down */ });
config.events.serverStopped(() -> { /* stopped */ });
config.events.handlerAdded(info -> { /* route registered */ });
config.events.wsHandlerAdded(info -> { /* ws route registered */ });
```

## Single page application (SPA) support
Serve a single HTML file for all unmatched paths under a root (for Vue, React, etc.):

```java
config.spaRoot.addFile("/", "/public/index.html");
// or with dynamic handler
config.spaRoot.addHandler("/", ctx -> ctx.html("..."));
```

## JavalinVue
Server-side Vue.js integration — no build pipeline needed:

```java
config.registerPlugin(new JavalinVuePlugin());
config.routes.get("/my-page", new VueComponent("my-page"));
```

Vue files go in `src/main/resources/vue/components/`. Pass server state to Vue:
```java
config.registerPlugin(new JavalinVuePlugin(vue -> {
    vue.stateFunction = ctx -> Map.of("user", getUser(ctx));
}));
// Access in Vue template: {{ $javalin.state.user }}
```

## OpenAPI
API documentation is available via the `javalin-openapi` plugin (separate repository).
See [javalin.io/plugins/openapi](https://javalin.io/plugins/openapi) for setup, `@OpenApi` annotations, and Swagger UI integration.

## Testing with JavalinTest
Use `javalin-testtools` (included in `javalin-bundle`) for integration tests. `JavalinTest.test()` starts a real server and provides an HTTP client:

```java
import io.javalin.testtools.JavalinTest;
import static org.assertj.core.api.Assertions.assertThat;

Javalin app = Javalin.create(config -> {
    config.router.apiBuilder(() -> {
        get("/users", ctx -> ctx.json(userService.getAll()));
        post("/users", ctx -> {
            User user = ctx.bodyAsClass(User.class);
            userService.create(user);
            ctx.status(201);
        });
    });
});

@Test
public void GET_users_returns_200() {
    JavalinTest.test(app, (server, client) -> {
        assertThat(client.get("/users").code()).isEqualTo(200);
    });
}

@Test
public void POST_users_creates_user() {
    JavalinTest.test(app, (server, client) -> {
        var response = client.post("/users", new User("Alice"));
        assertThat(response.code()).isEqualTo(201);
    });
}
```

The `client` supports `get()`, `post()`, `put()`, `patch()`, `delete()` — all return an OkHttp `Response` with `.code()` and `.body().string()`. Each test gets a fresh server instance on a random port.

## Context methods quick reference
Request info:
- `ctx.body()` — request body as string
- `ctx.bodyAsClass(MyClass.class)` — deserialize JSON body
- `ctx.pathParam("id")` — path parameter (e.g., `/users/{id}`)
- `ctx.queryParam("name")` — query parameter (e.g., `?name=alice`)
- `ctx.formParam("field")` — form parameter
- `ctx.header("X-Custom")` — request header
- `ctx.cookie("name")` — cookie value
- `ctx.uploadedFile("file")` — single uploaded file
- `ctx.uploadedFiles("files")` — multiple uploaded files
- `ctx.attribute("key", value)` / `ctx.attribute("key")` — request-scoped attributes (share data between handlers)
- `ctx.sessionAttribute("key")` — session attribute
- `ctx.method()`, `ctx.url()`, `ctx.ip()`, `ctx.contentType()` — request metadata

Response:
- `ctx.result("text")` — set text response
- `ctx.json(myObject)` — serialize to JSON response
- `ctx.html("<h1>Hi</h1>")` — set HTML response
- `ctx.status(201)` — set status code
- `ctx.header("X-Custom", "value")` — set response header
- `ctx.cookie("name", "value")` — set cookie
- `ctx.redirect("/path")` — redirect
- `ctx.render("template.html", model)` — render template
- `ctx.contentType("application/json")` — set content type

## Important Javalin 7 changes (common pitfalls)
1. **Routes MUST be inside `config.routes`** — you cannot add routes after `.start()`. This is the biggest v7 change.
2. **`app.start()` no longer returns `this`** — chain off `Javalin.create()` instead of storing and calling start separately.
3. **Template rendering is modular** — add `javalin-rendering-{engine}` artifacts explicitly (e.g., `javalin-rendering-jte`).
4. **Jetty 12** — if configuring Jetty directly, use Jetty 12 APIs.
5. **Java 17+** is required.
# ╔══════════════════════════════════════════════════════════════╗
# ║          END OF JAVALIN FRAMEWORK INSTRUCTIONS               ║
# ╚══════════════════════════════════════════════════════════════╝

Add to your GEMINI.md in your project root.

Like Javalin?
Star us 😊