diff --git a/_config.yml b/_config.yml index 9e1618c..c7dbd9e 100644 --- a/_config.yml +++ b/_config.yml @@ -2,12 +2,35 @@ javalinThreeVersion: 3.13.13 javalinFourVersion: 4.6.7 javalinFiveVersion: 5.6.4 javalinSixVersion: 6.7.0 -javalinversion: 6.7.0 +javalinversion: 7.0.0 slf4jversion: 2.0.17 repourl: https://github.com/javalin/website description: Javalin - A lightweight Java and Kotlin web framework. Create REST APIs in Java or Kotlin easily. +url: "https://javalin.io" baseurl: "" # the subpath of your site, e.g. /blog +defaults: + - scope: + path: "_posts/news/pre-3.0" + values: + noindex: true + sitemap: false + - scope: + path: "_posts/news/pre-4.0" + values: + noindex: true + sitemap: false + - scope: + path: "_posts/news/pre-5.0" + values: + noindex: true + sitemap: false + - scope: + path: "_posts/news/pre-6.0" + values: + noindex: true + sitemap: false + # Build settings markdown: kramdown sass: diff --git a/_includes/jsonld.html b/_includes/jsonld.html new file mode 100644 index 0000000..2f6e96c --- /dev/null +++ b/_includes/jsonld.html @@ -0,0 +1,91 @@ +{% if page.splash %} + +{% elsif page.title %} +{% capture page_description %}{% if page.description %}{{ page.description }}{% elsif page.excerpt %}{{ page.excerpt | strip_html | strip_newlines | truncate: 160 }}{% else %}{{ site.description }}{% endif %}{% endcapture %} +{% capture canonical_url %}{{ page.url | replace:'index.html','' | prepend: site.baseurl | prepend: site.url }}{% endcapture %} +{% if page.layout == 'blogpost' or page.layout == 'news' or page.layout == 'tutorial' %} + +{% else %} + +{% endif %} +{% endif %} + + diff --git a/_includes/landing.css b/_includes/landing.css index 87fe993..9833f4d 100644 --- a/_includes/landing.css +++ b/_includes/landing.css @@ -72,7 +72,7 @@ header.top-nav-header nav ul.top-nav li a:hover { } .bluepart.blackpart .multitab-code { - max-width: 590px; + max-width: 640px; } .bluepart .multitab-code { diff --git a/_includes/landing/section-4-server-and-api.html b/_includes/landing/section-4-server-and-api.html index 06f9a1b..2687198 100644 --- a/_includes/landing/section-4-server-and-api.html +++ b/_includes/landing/section-4-server-and-api.html @@ -10,7 +10,7 @@ config.http.asyncTimeout = 10_000L; config.staticFiles.add("/public"); config.staticFiles.enableWebjars(); - config.router.apiBuilder(() -> { + config.routes.apiBuilder(() -> { path("/users", () -> { get(UserController::getAll); post(UserController::create); @@ -35,7 +35,7 @@ config.http.asyncTimeout = 10_000L config.staticFiles.add("/public") config.staticFiles.enableWebjars() - config.router.apiBuilder { + config.routes.apiBuilder { path("/users") { get(UserController::getAll) post(UserController::create) diff --git a/_includes/landing/section-5-community.html b/_includes/landing/section-5-community.html index fc0869d..3193c07 100644 --- a/_includes/landing/section-5-community.html +++ b/_includes/landing/section-5-community.html @@ -5,7 +5,7 @@
-
1M+
+
2M+
Monthly downloads
@@ -19,28 +19,28 @@
-
8.1k
+
8.2k
GitHub Stars
-
2,396
+
2,466
Commits
-
1,141
+
1,163
Pull Requests
-
199
+
202
Contributors
@@ -54,14 +54,14 @@
-
632
+
638
Forks
-
153
+
154
Releases
@@ -72,5 +72,5 @@
Open Issues
-

As of November 2025

+

As of February 2026

diff --git a/_includes/macros/gettingStarted.md b/_includes/macros/gettingStarted.md index b81d035..1d55d02 100644 --- a/_includes/macros/gettingStarted.md +++ b/_includes/macros/gettingStarted.md @@ -2,9 +2,9 @@ import io.javalin.Javalin; void main() { - var app = Javalin.create(/*config*/) - .get("/", ctx -> ctx.result("Hello World")) - .start(7070); + var app = Javalin.create(config -> { + config.routes.get("/", ctx -> ctx.result("Hello World")); + }).start(7070); } {% endcapture %} @@ -12,9 +12,9 @@ void main() { import io.javalin.Javalin fun main() { - val app = Javalin.create(/*config*/) - .get("/") { ctx -> ctx.result("Hello World") } - .start(7070) + val app = Javalin.create { config -> + config.routes.get("/") { ctx -> ctx.result("Hello World") } + }.start(7070) } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} diff --git a/_includes/macros/gettingStarted6.md b/_includes/macros/gettingStarted6.md new file mode 100644 index 0000000..14267a1 --- /dev/null +++ b/_includes/macros/gettingStarted6.md @@ -0,0 +1,19 @@ +{% capture java %} +import io.javalin.Javalin; + +public static void main(String[] args) { + var app = Javalin.create().start(7070); + app.get("/", ctx -> ctx.result("Hello World")); +} +{% endcapture %} + +{% capture kotlin %} +import io.javalin.Javalin + +fun main() { + val app = Javalin.create().start(7070) + app.get("/") { ctx -> ctx.result("Hello World") } +} +{% endcapture %} +{% include macros/docsSnippet.html java=java kotlin=kotlin %} + diff --git a/_includes/macros/gettingStarted7.md b/_includes/macros/gettingStarted7.md deleted file mode 100644 index fdcdf07..0000000 --- a/_includes/macros/gettingStarted7.md +++ /dev/null @@ -1,21 +0,0 @@ -{% capture java %} -import io.javalin.Javalin; - -void main() { - var app = Javalin.create(config -> { - config.routes.get("/", ctx -> ctx.result("Hello World")); - }).start(7070); -} -{% endcapture %} - -{% capture kotlin %} -import io.javalin.Javalin - -fun main() { - val app = Javalin.create { config -> - config.routes.get("/") { ctx -> ctx.result("Hello World") } - }.start(7070) -} -{% endcapture %} -{% include macros/docsSnippet.html java=java kotlin=kotlin %} - diff --git a/_includes/notificationBanner.html b/_includes/notificationBanner.html index 015da31..6fe6fad 100644 --- a/_includes/notificationBanner.html +++ b/_includes/notificationBanner.html @@ -1,24 +1,26 @@ diff --git a/_layouts/default.html b/_layouts/default.html index 772a2bc..11a4b59 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -5,20 +5,28 @@ - - + + + + + + + + {% if page.noindex %}{% endif %} {% if page.title %}{{ page.title }} - {% endif %}Javalin - A lightweight Java and Kotlin web framework - + + + {% include jsonld.html %}
diff --git a/_posts/blog/2025-12-27-using-augment-for-javalin-7.md b/_posts/blog/2025-12-27-using-augment-for-javalin-7.md index 87fe18c..80208d2 100644 --- a/_posts/blog/2025-12-27-using-augment-for-javalin-7.md +++ b/_posts/blog/2025-12-27-using-augment-for-javalin-7.md @@ -24,7 +24,7 @@ Javalin is a passion project which I maintain in my free time. I have a full-tim This fall, I started trying out various agentic pair programming tools to see if they could help me get more done, and the results have been pretty positive. Major refactorings that would have been too daunting to attempt are now feasible. Nice-to-have improvements actually get done instead of sitting on the backlog forever. -I've tested three tools extensively for Javalin 7: GitHub Copilot, Google Antigravity, and Augment. After months of real-world use on complex refactoring work, my clear recommendation is Augment. It's more expensive than the alternatives, but for the kind of work I do it consistently delivers better results. This post shares some examples and advice for anyone considering using AI agents for their own projects. +I've tested three tools extensively for Javalin 7: GitHub Copilot, Google Antigravity, and Augment. This post shares some examples and observations for anyone considering using AI agents for their own projects. ## How I work with AI agents @@ -40,7 +40,7 @@ For larger tasks, I usually spawn two agents: one to write code, and one to disc ### Code quality and context understanding -The code quality is genuinely good. Out of Javalin's 200+ contributors, I would say Augment is a top ten performer. It understands context deeply enough to make architectural decisions that respect the existing codebase structure and project philosophy. +The code quality has generally been good. It understands context deeply enough to make architectural decisions that respect the existing codebase structure and project philosophy. ### Automatic project conventions @@ -123,9 +123,9 @@ For me, Augment's value is strongest for: Agentic pair programming has changed how I maintain Javalin. Without it, the project would make slower progress. -After extensive testing of multiple AI coding assistants, I recommend Augment for serious development work. It costs more than alternatives, but for complex refactoring, architectural changes, and maintaining large codebases, it consistently delivers better results. The [MCP integration with JetBrains IDEs](https://www.jetbrains.com/help/idea/mcp-server.html) is particularly powerful. +For the work I do on Javalin β€” complex refactoring, architectural changes, large codebases β€” Augment has been the most effective of the three tools I tested. It costs more than the alternatives, so whether it's worth it depends on the kind of work you're doing. The [MCP integration with JetBrains IDEs](https://www.jetbrains.com/help/idea/mcp-server.html) is particularly useful. -If you're doing autocomplete-style coding, stick with Copilot. If you're doing complex refactoring or maintaining a large codebase solo, Augment is worth it. +If you're doing autocomplete-style coding, Copilot works well and costs less. If you're doing complex refactoring or maintaining a large codebase solo, Augment may be worth evaluating. Your mileage may vary, but this is what worked for me. diff --git a/_posts/news/pre-8.0/2026-01-20-javalin-7.0.0-released.md b/_posts/news/pre-8.0/2026-02-22-javalin-7.0.0-released.md similarity index 73% rename from _posts/news/pre-8.0/2026-01-20-javalin-7.0.0-released.md rename to _posts/news/pre-8.0/2026-02-22-javalin-7.0.0-released.md index 5ddae13..50d9715 100644 --- a/_posts/news/pre-8.0/2026-01-20-javalin-7.0.0-released.md +++ b/_posts/news/pre-8.0/2026-02-22-javalin-7.0.0-released.md @@ -1,12 +1,12 @@ --- hidewhatsjavalin: true -permalink: /news/javalin-7.0.0-alpha.html +permalink: /news/javalin-7.0.0-stable.html layout: default category: news -date: 2026-01-20 +date: 2026-02-22 version: 7.0.0 -title: Javalin 7.0 alpha is ready! -summary: We're happy to announce the release of Javalin 7.0! +title: Javalin 7.0 is released! +summary: Javalin 7.0 is out, built on Kotlin 2, Java 17, and Jetty 12. --- {% include socialButtons.html %} @@ -14,23 +14,21 @@ summary: We're happy to announce the release of Javalin 7.0!

Introducing Javalin 7 - (TBD, 2026) + (February 22, 2026)

-Javalin is a Java and Kotlin web framework focused on simplicity and seamless interoperability. -Built as a thin layer on top of the Jetty web server, it concentrates on the web layer without -unnecessary abstraction. +Javalin is a Java and Kotlin web framework focused on simplicity and developer productivity. +It's a thin programmatic layer on top of Jetty, meaning no annotations, no magic, no unnecessary abstraction, just straightforward HTTP. -Much of Javalin’s success comes from the exceptionally supportive JVM open-source community. -After nearly eight years, the project sees over 1 million monthly downloads, 8.1k GitHub stars, -199 contributors, and 12k dependent projects. The core module is just ~10k lines of code, -backed by ~14k lines of tests, nearly 1k full integration tests, each starting and stopping -a Javalin instance to validate real-world behavior. - -**Let's have a look at Javalin 7!** +Javalin 7 requires Java 17 and Jetty 12, and brings an improved +configuration model, a more consistent plugin API, and a cleaner overall architecture. +It's the result of nearly nine years of community feedback, with over 2 million monthly downloads, 8.2k GitHub stars, and contributions from 202 developers around the world. ## Hello World -Javalin's main goal is simplicity and developer productivity. -The "Hello World" example in Javalin 7 reflects the new upfront configuration approach: +Add the dependency, then write your first Javalin app in Java or Kotlin: + +```java +implementation("io.javalin:javalin:{{ site.javalinversion }}") +``` {% capture java %} void main() { var app = Javalin.create(config -> { @@ -48,11 +46,13 @@ fun main() { {% include macros/docsSnippet.html java=java kotlin=kotlin %} ## Building REST APIs with Javalin -Creating a REST API with Javalin is very straightforward. -Here's a complete server with a CRUD API: +Creating an application with Javalin is very straightforward. +Here's a complete server with an API, static files, and WebSockets: {% capture java %} -var app = Javalin.create(config -> { +var app = Javalin.start(config -> { + config.jetty.port = 7070; + config.staticFiles.add("/public", Location.CLASSPATH); config.routes.apiBuilder(() -> { path("users", () -> { get(UserController::getAll); @@ -63,11 +63,16 @@ var app = Javalin.create(config -> { delete(UserController::delete); }); }); + ws("/events", ws -> { + ws.onMessage(ctx -> ctx.send(ctx.message())); + }); }); -}).start(7070); +}); {% endcapture %} {% capture kotlin %} -val app = Javalin.create { config -> +val app = Javalin.start { config -> + config.jetty.port = 7070 + config.staticFiles.add("/public", Location.CLASSPATH) config.routes.apiBuilder { path("users") { get(UserController::getAll) @@ -78,8 +83,11 @@ val app = Javalin.create { config -> delete(UserController::delete) } } + ws("/events") { ws -> + ws.onMessage { ctx -> ctx.send(ctx.message()) } + } } -}.start(7070) +} {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -186,11 +194,8 @@ var app = Javalin.create(config -> { // Static files config.staticFiles.add("/public", Location.CLASSPATH); - // Custom resource handler (optional, for Jetty-free static file serving) - config.resourceHandler(new JavalinStaticResourceHandler()); - // Jetty configuration - config.jetty.defaultPort = 8080; + config.jetty.port = 8080; }).start(); {% endcapture %} {% capture kotlin %} @@ -206,11 +211,8 @@ val app = Javalin.create { config -> // Static files config.staticFiles.add("/public", Location.CLASSPATH) - // Custom resource handler (optional, for Jetty-free static file serving) - config.resourceHandler(JavalinStaticResourceHandler()) - // Jetty configuration - config.jetty.defaultPort = 8080 + config.jetty.port = 8080 }.start() {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -218,20 +220,43 @@ val app = Javalin.create { config -> For a full list of configuration options, see the [documentation](/documentation#configuration). ## Plugins -Javalin's plugin system enforces plugin authors to provide a consistent API: +Javalin's plugin system enforces a consistent, consumer-based API, the same pattern used throughout the rest of Javalin's configuration. +To create a plugin, extend `Plugin` and override `onStart`: + +{% capture java %} +class ExamplePlugin extends Plugin { + ExamplePlugin(Consumer userConfig) { super(userConfig, new Config()); } + + @Override + public void onStart(JavalinState state) { + state.routes.get("/example", ctx -> ctx.result(pluginConfig.message)); + } + + public static class Config { + public String message = "Hello, plugin!"; + } +} +{% endcapture %} +{% capture kotlin %} +class ExamplePlugin(userConfig: Consumer? = null) : Plugin(userConfig, Config()) { + override fun onStart(state: JavalinState) { + state.routes.get("/example") { ctx -> ctx.result(pluginConfig.message) } + } + class Config { var message = "Hello, plugin!" } +} +{% endcapture %} +{% include macros/docsSnippet.html java=java kotlin=kotlin %} + +Plugins are registered via `config.registerPlugin`, and users can configure them inline: {% capture java %} Javalin.create(config -> { - config.registerPlugin(new ExamplePlugin(exampleConfig -> { - exampleConfig.exampleSetting = "example"; - })); + config.registerPlugin(new ExamplePlugin(c -> c.message = "Hi!")); }); {% endcapture %} {% capture kotlin %} Javalin.create { config -> - config.registerPlugin(ExamplePlugin { exampleConfig -> - exampleConfig.exampleSetting = "example" - }) + config.registerPlugin(ExamplePlugin { c -> c.message = "Hi!" }) } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -255,5 +280,13 @@ If you want to stay up to date, please follow us on [Twitter](https://twitter.co line-height: 1.7; text-wrap: pretty; } +.release-date { + font-weight: 400; + font-size: 60%; + color: rgba(0,0,0,0.5); +} +.dark-mode .release-date { + color: rgba(255,255,255,0.5); +} diff --git a/img/news/javalin7.png b/img/news/javalin7.png index 5bdbff8..112a9c5 100644 Binary files a/img/news/javalin7.png and b/img/news/javalin7.png differ diff --git a/javalin-7-release-checklist.md b/javalin-7-release-checklist.md index f316333..03e45e0 100644 --- a/javalin-7-release-checklist.md +++ b/javalin-7-release-checklist.md @@ -3,29 +3,26 @@ ## Documentation Updates ### Core Documentation -- [x] `pages/docs/docs-future-7.md` - Updated with all Javalin 7 changes -- [x] `pages/docs/migration-guide-6-7.md` - Migration guide created -- [x] `_includes/macros/gettingStarted7.md` - Created Javalin 7 getting started macro -- [x] `_config.yml` - Added `javalinSixVersion: 6.7.0` config variable -- [ ] Rename `pages/docs/docs-future-7.md` to `pages/docs/docs.md` (and move current docs.md to docs-past-6-X.md) -- [ ] Update `pages/docs/migration-guide-6-7.md` link references (currently points to `/migration-guide-javalin-6-to-7`) +- [x] `pages/docs/docs-future-7.md` renamed to `pages/docs/docs.md`; old `docs.md` moved to `docs-past-6-X.md` +- [x] `pages/docs/migration-guide-6-7.md` - Migration guide created and link references updated +- [x] `pages/docs/docs.md` - Cleaned up intro (removed "Major Changes in Javalin 7" bullet list, added Java 17+/Jetty 12+ statement) ### Plugin Documentation -- [ ] `pages/plugins/how-to.md` - Update route examples from `app.*` to `config.routes.*` -- [ ] `pages/plugins/javalinvue.md` - Update to show JavalinVue as a plugin, update route examples -- [ ] `pages/plugins/rendering.md` - Update route examples if present -- [ ] `pages/plugins/cors.md` - Update route examples if present -- [ ] `pages/plugins/devlogging.md` - Update route examples if present -- [ ] `pages/plugins/graphql.md` - Update route examples if present -- [ ] `pages/plugins/micrometer.md` - Update route examples if present -- [ ] `pages/plugins/routeoverview.md` - Update route examples if present -- [ ] `pages/plugins/ssl-helpers.md` - Update route examples if present +- [x] `pages/plugins/how-to.md` - Fixed API signatures (`onStart(JavalinState)`, `ContextPlugin`, `state.routes.before(...)`) +- [x] `pages/plugins/javalinvue.md` - Already uses `config.registerPlugin(new JavalinVuePlugin())` and `config.routes.get(...)` βœ“ +- [x] `pages/plugins/rendering.md` - No old route examples present βœ“ +- [x] `pages/plugins/cors.md` - No old route examples present βœ“ +- [x] `pages/plugins/devlogging.md` - No old route examples present βœ“ +- [x] `pages/plugins/graphql.md` - No old route examples present βœ“ +- [x] `pages/plugins/micrometer.md` - No old route examples present βœ“ +- [x] `pages/plugins/routeoverview.md` - No old route examples present βœ“ +- [x] `pages/plugins/ssl-helpers.md` - No old route examples present βœ“ ### Other Pages -- [ ] `pages/for-educators.md` - Update getting started example -- [ ] `pages/comparison-to-spark.md` - Update route examples if present -- [ ] `pages/index.md` - Update homepage examples if present -- [ ] `pages/download.md` - Verify version references +- [x] `pages/index.md` - Community stats updated (2M+ downloads, 8.2k stars, 638 forks, etc. β€” "As of February 2026") +- [x] `pages/comparison-to-spark.md` - Already uses `config.routes.get(...)` βœ“ +- [x] `pages/for-educators.md` - Uses `{% include macros/gettingStarted.md %}` which now points to v7 βœ“ +- [x] `pages/download.md` - Uses `{{site.javalinversion}}` (resolves to 7.0.0) and `mavenDep.md` macro βœ“ ### Tutorial Updates - [x] All tutorials in `_posts/tutorials/` locked to `javalinSixVersion` (6.7.0) @@ -34,41 +31,34 @@ ## Configuration Updates ### Version Variables +- [x] `_config.yml` - `javalinversion` updated to `7.0.0` - [x] `_config.yml` - `javalinSixVersion: 6.7.0` added -- [ ] `_config.yml` - Update `javalinversion` to `7.0.0` (or appropriate version) when ready to release -- [ ] Verify all version references throughout the site ### Macros and Includes -- [x] `_includes/macros/gettingStarted7.md` - Created -- [ ] `_includes/macros/gettingStarted.md` - Update to Javalin 7 syntax when ready -- [ ] `_includes/macros/mavenDep.md` - Verify it works with version override -- [ ] Review other macros in `_includes/macros/` for needed updates +- [x] `_includes/macros/gettingStarted.md` - Updated to Javalin 7 syntax +- [x] `_includes/macros/gettingStarted6.md` - Created for v6 archive docs ## Content Review ### Code Examples -- [ ] Search for all `app.get(` references and verify they're either updated or intentionally left for old versions -- [ ] Search for all `app.post(` references -- [ ] Search for all `app.before(` references -- [ ] Search for all `app.after(` references -- [ ] Search for all `app.ws(` references -- [ ] Search for all `ctx.matchedPath()` references - should be `ctx.endpoint().path()` -- [ ] Search for all `app.events(` references - should be `config.events.*` -- [ ] Search for all `app.unsafeConfig().pvt` references - should be `app.unsafe` -- [ ] Search for all `config.vue` references - should be JavalinVue plugin +- [x] `app.get(`, `app.post(`, `app.before(`, `app.after(`, `app.ws(` β€” only present in `docs-past-*` archives (intentional) βœ“ +- [x] `ctx.matchedPath()` β€” only in archives and migration guide (showing oldβ†’new) βœ“ +- [x] `app.events(` β€” only in archives and migration guide (showing oldβ†’new) βœ“ +- [x] `app.unsafeConfig().pvt` β€” only in migration guide (showing oldβ†’new) βœ“ +- [x] `config.vue` β€” only in archives and migration guides (intentional) βœ“ ### Breaking Changes Coverage -- [ ] Routes configured upfront in `config.routes.*` - documented -- [ ] Lifecycle events in `config.events.*` - documented -- [ ] `ctx.matchedPath()` β†’ `ctx.endpoint().path()` - documented -- [ ] JavalinVue as plugin - documented -- [ ] `app.unsafeConfig().pvt` β†’ `app.unsafe` - documented -- [ ] `config.router.javaLangErrorHandler` - documented -- [ ] Jetty 12 / Jakarta servlet packages - documented -- [ ] Java 17 requirement - documented -- [ ] `createAndStart()` removal - documented -- [ ] HandlerType as record - documented -- [ ] Template rendering modules - documented +- [x] Routes configured upfront in `config.routes.*` β€” docs.md prominently documents this with a callout box βœ“ +- [x] Lifecycle events in `config.events.*` β€” docs.md has full `config.events.*` examples βœ“ +- [x] `ctx.matchedPath()` β†’ `ctx.endpoint().path()` β€” migration guide + docs.md uses `ctx.endpoint().path()` βœ“ +- [x] JavalinVue as plugin β€” docs.md explicitly notes it, `javalinvue.md` uses `config.registerPlugin(...)` βœ“ +- [x] `app.unsafeConfig().pvt` β†’ `app.unsafe` β€” migration guide covers with before/after examples βœ“ +- [x] `config.router.javaLangErrorHandler` β€” covered under the `app.unsafe` migration section βœ“ +- [x] Jetty 12 / Jakarta servlet packages β€” docs.md + migration guide have dedicated sections βœ“ +- [x] Java 17 requirement β€” docs.md intro states it βœ“ +- [x] `createAndStart()` removal β€” migration guide has a dedicated section βœ“ +- [x] HandlerType as record β€” migration guide "Other changes" section βœ“ +- [x] Template rendering modules β€” docs.md references `javalin-rendering` artifact βœ“ ## Testing @@ -85,17 +75,30 @@ - [ ] Test on Safari - [ ] Test on mobile devices +## Release Post + +- [x] `_posts/news/pre-8.0/2026-02-22-javalin-7.0.0-released.md` - Written and published +- [x] Release post intro, Hello World + Gradle snippet, REST APIs, Static Files, WebSockets, Config, Plugins sections all updated +- [x] Dark mode fix for date subtitle (`.release-date` CSS class) +- [x] `_posts/blog/2025-12-27-using-augment-for-javalin-7.md` - Augment endorsement toned down + +## Notification Banner + +- [x] `_includes/notificationBanner.html` - Updated for Javalin 7 (uncommented, new content and link) +- [x] Banner restricted to `/documentation*` and `/tutorials*` pages only +- [x] Background changed to green (`#9fff82`) +- [x] Close button replaced with SVG X icon +- [x] Top-left corner clipped (`clip-path: polygon(...)`) + ## Pre-Release Tasks ### Communication Preparation -- [ ] Draft release announcement blog post for website - [ ] Prepare GitHub release notes with changelog - [ ] Draft Twitter/X announcement (with key highlights) - [ ] Draft Reddit post for r/java and r/programming - [ ] Draft Hacker News submission (title and description) - [ ] Prepare LinkedIn announcement - [ ] Draft Discord/Slack community announcements -- [ ] Update README if needed - [ ] Notify key contributors and maintainers ### Final Checks @@ -108,10 +111,9 @@ ## Release Day ### Website Updates -- [ ] Rename `docs-future-7.md` to `docs.md` -- [ ] Move old `docs.md` to `docs-past-6-X.md` -- [ ] Update `_config.yml` - set `javalinversion: 7.0.0` -- [ ] Update `_includes/macros/gettingStarted.md` to use Javalin 7 syntax +- [x] Rename `docs-future-7.md` to `docs.md` and move old `docs.md` to `docs-past-6-X.md` +- [x] Update `_config.yml` - `javalinversion: 7.0.0` +- [x] Update `_includes/macros/gettingStarted.md` to use Javalin 7 syntax - [ ] Commit and push all changes - [ ] Verify deployment diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..c9e1533 --- /dev/null +++ b/llms.txt @@ -0,0 +1,34 @@ +# Javalin + +> Javalin is a lightweight web framework for Java and Kotlin. Version 7 (current) requires Java 17+ and runs on Jetty 12. All configuration β€” routes, handlers, plugins, lifecycle events β€” is passed upfront in a single config block when creating the instance. + +Key facts for v7: +- Minimum Java version is 17 (was 11 in v6) +- Built on Jetty 12 (was Jetty 11 in v6) +- All setup is done inside `Javalin.create(config -> { ... })` β€” routes and handlers are no longer added after `.start()` +- Template engine support is now modular: add `javalin-rendering-{engine}` instead of a monolithic `javalin-rendering` artifact +- Kotlin support targets Kotlin 2.0 + +## Docs + +- [Documentation](https://javalin.io/documentation): Full v7 reference β€” routing, handlers, WebSockets, validation, access management, SSE, configuration, lifecycle, plugins +- [Migration guide: v6 β†’ v7](https://javalin.io/migration-guide-javalin-6-to-7): Required reading for anyone upgrading from v6; covers all breaking changes including the upfront-config model, Jetty 12, and Java 17 minimum + +## Plugins + +- [CORS plugin](https://javalin.io/plugins/cors): Configure CORS policies; replaces the old `app.enableCors()` API +- [OpenAPI plugin](https://javalin.io/plugins/openapi): Generate OpenAPI/Swagger documentation from annotated route handlers +- [Rendering plugin](https://javalin.io/plugins/rendering): Template engine support (Freemarker, Thymeleaf, Mustache, Velocity, Pebble, JTE, Handlebars, CommonMark) via per-engine `javalin-rendering-{engine}` modules +- [JavalinVue plugin](https://javalin.io/plugins/javalinvue): Server-driven Vue.js component loading without a build step +- [SSL plugin](https://javalin.io/plugins/ssl-helpers): HTTPS/HTTP2/mTLS configuration helper +- [GraphQL plugin](https://javalin.io/plugins/graphql): Add a GraphQL endpoint +- [Micrometer plugin](https://javalin.io/plugins/micrometer): Expose Micrometer metrics +- [Route overview plugin](https://javalin.io/plugins/routeoverview): Expose a JSON overview of all registered routes +- [Dev logging plugin](https://javalin.io/plugins/devlogging): Pretty-print incoming requests and responses during development +- [How to create plugins](https://javalin.io/plugins/how-to): Guide for writing custom Javalin plugins + +## Optional + +- [Archived v6 docs](https://javalin.io/archive/docs/v6.X.html): Full reference for Javalin 6 (Java 11+, Jetty 11) +- [Archived v5 docs](https://javalin.io/archive/docs/v5.6.X.html): Full reference for Javalin 5 + diff --git a/pages/about.md b/pages/about.md index 34fed34..d037df2 100644 --- a/pages/about.md +++ b/pages/about.md @@ -3,6 +3,7 @@ layout: default title: About rightmenu: false permalink: /about +description: "About Javalin β€” a lightweight Java and Kotlin web framework inspired by Sinatra and koa.js. Philosophy, API design, and Java/Kotlin interoperability." ---

About Javalin

diff --git a/pages/community.md b/pages/community.md index 885ece7..9732244 100644 --- a/pages/community.md +++ b/pages/community.md @@ -3,6 +3,7 @@ layout: default title: Community rightmenu: false permalink: /community +description: "Join the Javalin community. Discord server, GitHub discussions, contributors, and support resources." --- {% include notificationBanner.html %} diff --git a/pages/comparison-to-spark.md b/pages/comparison-to-spark.md index 66dea36..366b43f 100644 --- a/pages/comparison-to-spark.md +++ b/pages/comparison-to-spark.md @@ -3,6 +3,7 @@ layout: default title: Comparison to SparkJava rightmenu: false permalink: /comparisons/sparkjava +description: "Comparison between Javalin and SparkJava. Differences in API design, features, async support, and WebSocket handling." ---

SparkJava and Javalin comparison

@@ -36,8 +37,8 @@ Javalin now offers a lot of features that are not available in Spark: Javalin has the concept of a **Handler**. A **Handler** is void and takes a **Context**, which wraps **HttpServletRequest** and **HttpServletResponse**. You operate on both the request and response through this **Context**. ```java -javalin.get("/path", ctx -> ctx.result("Hello, World!")); -javalin.after("/path", ctx -> ctx.result("Actually, nevermind...")); +config.routes.get("/path", ctx -> ctx.result("Hello, World!")); +config.routes.after("/path", ctx -> ctx.result("Actually, nevermind...")); ``` Spark on the other hand has **Routes** and **Filters** . Both **Route** and **Filter** in Spark take @@ -57,7 +58,7 @@ Javalin's approach leads to a much more consistent API, both for the previous an ### Redirects ```java -javalin.get("/", ctx -> ctx.redirect("/new-path")); +config.routes.get("/", ctx -> ctx.redirect("/new-path")); // vs Spark.get("/", (req, res) -> { res.redirect("/new-path"); // can't return here, the redirect method is void @@ -67,7 +68,7 @@ Spark.get("/", (req, res) -> { ### JSON mapping ```java -javalin.get("/", ctx -> ctx.json(object)); +config.routes.get("/", ctx -> ctx.json(object)); // vs Spark.get("/", (req, res) -> object, new JsonTransformer()); ``` @@ -75,7 +76,7 @@ Spark.get("/", (req, res) -> object, new JsonTransformer()); ### Templates ```java -javalin.get("/", ctx -> ctx.render("path", model)); +config.routes.get("/", ctx -> ctx.render("path", model)); // vs Spark.get("/", (req, res) -> new ModelAndView(model, "path"), new TemplateEngine()); ``` diff --git a/pages/docs/docs-past-1-7-0.md b/pages/docs/docs-past-1-7-0.md index 80c5b78..860d8b7 100644 --- a/pages/docs/docs-past-1-7-0.md +++ b/pages/docs/docs-past-1-7-0.md @@ -3,6 +3,8 @@ layout: default title: Archive - v1 documentation rightmenu: true permalink: /archive/docs/v1.7.0.html +noindex: true +sitemap: false ---
diff --git a/pages/docs/docs-past-2-8-0.md b/pages/docs/docs-past-2-8-0.md index 0ccdde3..569dc2b 100644 --- a/pages/docs/docs-past-2-8-0.md +++ b/pages/docs/docs-past-2-8-0.md @@ -3,6 +3,8 @@ layout: default title: Archive - v2 documentation rightmenu: true permalink: /archive/docs/v2.8.0.html +noindex: true +sitemap: false --- {% include notificationBanner.html %} diff --git a/pages/docs/docs-past-3-13-X.md b/pages/docs/docs-past-3-13-X.md index 8738782..e9f4286 100644 --- a/pages/docs/docs-past-3-13-X.md +++ b/pages/docs/docs-past-3-13-X.md @@ -3,6 +3,8 @@ layout: default title: Archive - v3 documentation rightmenu: true permalink: /archive/docs/v3.13.X.html +noindex: true +sitemap: false --- {% include notificationBanner.html %} @@ -51,7 +53,7 @@ Add the dependency: {% include macros/mavenDep.md %} Start coding: -{% include macros/gettingStarted.md %} +{% include macros/gettingStarted6.md %} ## Handlers Javalin has three main handler types: before-handlers, endpoint-handlers, and after-handlers. diff --git a/pages/docs/docs-past-4-6-X.md b/pages/docs/docs-past-4-6-X.md index 6005d8b..ee99866 100644 --- a/pages/docs/docs-past-4-6-X.md +++ b/pages/docs/docs-past-4-6-X.md @@ -3,6 +3,8 @@ layout: default title: Archive - v4 Documentation rightmenu: true permalink: /archive/docs/v4.6.X.html +noindex: true +sitemap: false --- {% include notificationBanner.html %} @@ -51,7 +53,7 @@ Add the dependency: {% include macros/mavenDep.md %} Start coding: -{% include macros/gettingStarted.md %} +{% include macros/gettingStarted6.md %} ## Handlers Javalin has three main handler types: before-handlers, endpoint-handlers, and after-handlers. diff --git a/pages/docs/docs-past-5-6-x.md b/pages/docs/docs-past-5-6-x.md index 894bfcd..5b94d56 100644 --- a/pages/docs/docs-past-5-6-x.md +++ b/pages/docs/docs-past-5-6-x.md @@ -3,6 +3,8 @@ layout: default title: Archive - v5 Documentation rightmenu: true permalink: /archive/docs/v5.6.X.html +noindex: true +sitemap: false --- {% include notificationBanner.html %} @@ -81,7 +83,7 @@ Add the dependency: {% include macros/mavenDep.md %} Start coding: -{% include macros/gettingStarted.md %} +{% include macros/gettingStarted6.md %} ## Handlers Javalin has three main handler types: before-handlers, endpoint-handlers, and after-handlers. diff --git a/pages/docs/docs-future-7.md b/pages/docs/docs-past-6-X.md similarity index 86% rename from pages/docs/docs-future-7.md rename to pages/docs/docs-past-6-X.md index 4213799..f3e5948 100644 --- a/pages/docs/docs-future-7.md +++ b/pages/docs/docs-past-6-X.md @@ -1,8 +1,8 @@ --- layout: default -title: Documentation +title: Archive - v6 Documentation rightmenu: true -permalink: /docs-future +permalink: /archive/docs/v6.X.html --- {% include notificationBanner.html %} @@ -13,7 +13,6 @@ permalink: /docs-future - [Before](#before-handlers) - [Endpoint](#endpoint-handlers) - [After](#after-handlers) - - [Wrapper](#wrapper-handlers) - [Context (ctx)](#context) - [WebSockets](#websockets) - [Before](#wsbefore) @@ -66,36 +65,21 @@ permalink: /docs-future - [Documentation for previous versions](#documentation-for-previous-versions)
-

Documentation - Javalin 7

+

Documentation

-This documentation is for Javalin 7, which is currently in development. -For the current stable version (6.X), see the [current documentation](/documentation). +The documentation is for the latest version of Javalin, currently `{{site.javalinversion}}`. Javalin follows [semantic versioning](http://semver.org/), meaning there are no breaking -changes unless the major (leftmost) digit changes, for example `6.X.X` to `7.X.X`. - -### Major Changes in Javalin 7 - -- **Java 17 required** (previously Java 11) -- **Jetty 12** (previously Jetty 11) - servlet packages changed from `javax.servlet.*` to `jakarta.servlet.*` -- **Routes must be configured upfront** in `config.routes.*` instead of `app.*` - -See the [migration guide](/migration-guide-javalin-6-to-7) for complete details. +changes unless the major (leftmost) digit changes, for example `5.X.X` to `6.X.X`. {% include sponsorOrStar.html %} ## Getting Started Add the dependency: -{% assign javalinVersion = "7.0.0-alpha4" %} {% include macros/mavenDep.md %} Start coding: -{% include macros/gettingStarted7.md %} - -
-**Important change in Javalin 7:** Routes must now be defined in the `config.routes` block during application creation. -You can no longer add routes after calling `.start()`. See the [migration guide](/migration-guide-javalin-6-to-7) for details. -
+{% include macros/gettingStarted6.md %} ## Handlers Javalin has three main handler types: before-handlers, endpoint-handlers, and after-handlers. @@ -119,33 +103,33 @@ Before-handlers are matched before every request (including static files).
You might know before-handlers as filters, interceptors, or middleware from other libraries.
{% capture java %} -config.routes.before(ctx -> { +app.before(ctx -> { // runs before all requests }); -config.routes.before("/path/*", ctx -> { +app.before("/path/*", ctx -> { // runs before request to /path/* }); {% endcapture %} {% capture kotlin %} -config.routes.before { ctx -> +app.before { ctx -> // runs before all requests } -config.routes.before("/path/*") { ctx -> +app.before("/path/*") { ctx -> // runs before request to /path/* } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} In some cases, you might want to only run a before-handler if the request will be matched (not 404). -In this case you can use the `config.routes.beforeMatched` method: +In this case you can use the `app.beforeMatched` method: {% capture java %} -config.routes.beforeMatched(ctx -> { +app.beforeMatched(ctx -> { // runs before all matched requests (including static files) }); {% endcapture %} {% capture kotlin %} -config.routes.beforeMatched { ctx -> +app.beforeMatched { ctx -> // runs before all matched requests (including static files) } {% endcapture %} @@ -155,34 +139,31 @@ config.routes.beforeMatched { ctx -> ### Endpoint handlers Endpoint handlers are the main handler type, and defines your API. You can add a GET handler to serve data to a client, or a POST handler to receive some data. -Common methods are supported via `config.routes` (GET, POST, QUERY, PUT, PATCH, DELETE, HEAD, OPTIONS), -uncommon operations (TRACE, CONNECT) are supported via `config.routes.addHandler`. - -The [QUERY method](https://httpwg.org/http-extensions/draft-ietf-httpbis-safe-method-w-body.html) is similar to GET, -but allows a request body. This is useful for complex queries that don't fit in a URL. +Common methods are supported directly on the `Javalin` class (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS), +uncommon operations (TRACE, CONNECT) are supported via `Javalin#addHandler`. Endpoint-handlers are matched in the order they are defined.
You might know endpoint-handlers as routes or middleware from other libraries.
{% capture java %} -config.routes.get("/output", ctx -> { +app.get("/output", ctx -> { // some code ctx.json(object); }); -config.routes.post("/input", ctx -> { +app.post("/input", ctx -> { // some code ctx.status(201); }); {% endcapture %} {% capture kotlin %} -config.routes.get("/output") { ctx -> +app.get("/output") { ctx -> // some code ctx.json(object) } -config.routes.post("/input") { ctx -> +app.post("/input") { ctx -> // some code ctx.status(201) } @@ -191,18 +172,18 @@ config.routes.post("/input") { ctx -> Handler paths can include path-parameters. These are available via `ctx.pathParam("key")`: {% capture java %} -config.routes.get("/hello/{name}", ctx -> { // the {} syntax does not allow slashes ('/') as part of the parameter +app.get("/hello/{name}", ctx -> { // the {} syntax does not allow slashes ('/') as part of the parameter ctx.result("Hello: " + ctx.pathParam("name")); }); -config.routes.get("/hello/", ctx -> { // the <> syntax allows slashes ('/') as part of the parameter +app.get("/hello/", ctx -> { // the <> syntax allows slashes ('/') as part of the parameter ctx.result("Hello: " + ctx.pathParam("name")); }); {% endcapture %} {% capture kotlin %} -config.routes.get("/hello/{name}") { ctx -> // the {} syntax does not allow slashes ('/') as part of the parameter +app.get("/hello/{name}") { ctx -> // the {} syntax does not allow slashes ('/') as part of the parameter ctx.result("Hello: " + ctx.pathParam("name")) } -config.routes.get("/hello/") { ctx -> // the <> syntax allows slashes ('/') as part of the parameter +app.get("/hello/") { ctx -> // the <> syntax allows slashes ('/') as part of the parameter ctx.result("Hello: " + ctx.pathParam("name")) } {% endcapture %} @@ -211,13 +192,13 @@ config.routes.get("/hello/") { ctx -> // the <> syntax allows slashes ('/' Handler paths can also include wildcard parameters: {% capture java %} -config.routes.get("/path/*", ctx -> { // will match anything starting with /path/ - ctx.result("You are here because " + ctx.path() + " matches " + ctx.endpoint().path()); +app.get("/path/*", ctx -> { // will match anything starting with /path/ + ctx.result("You are here because " + ctx.path() + " matches " + ctx.matchedPath()); }); {% endcapture %} {% capture kotlin %} -config.routes.get("/path/*") { ctx -> // will match anything starting with /path/ - ctx.result("You are here because " + ctx.path() + " matches " + ctx.endpoint().path()) +app.get("/path/*") { ctx -> // will match anything starting with /path/ + ctx.result("You are here because " + ctx.path() + " matches " + ctx.matchedPath()) } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -230,87 +211,38 @@ After-handlers run after every request (even if an exception occurred)
You might know after-handlers as filters, interceptors, or middleware from other libraries.
{% capture java %} -config.routes.after(ctx -> { +app.after(ctx -> { // run after all requests }); -config.routes.after("/path/*", ctx -> { +app.after("/path/*", ctx -> { // runs after request to /path/* }); {% endcapture %} {% capture kotlin %} -config.routes.after { ctx -> +app.after { ctx -> // run after all requests } -config.routes.after("/path/*") { ctx -> +app.after("/path/*") { ctx -> // runs after request to /path/* } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} In some cases, you might want to only run an after-handler if the request will be matched (not 404). -In this case you can use the `config.routes.afterMatched` method: +In this case you can use the `app.afterMatched` method: {% capture java %} -config.routes.afterMatched(ctx -> { +app.afterMatched(ctx -> { // runs after all matched requests (including static files) }); {% endcapture %} {% capture kotlin %} -config.routes.afterMatched { ctx -> +app.afterMatched { ctx -> // runs after all matched requests (including static files) } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} -### Wrapper handlers -Wrapper-handlers run "around" endpoint handlers. The `HandlerWrapper` functional interface -receives an `Endpoint` (with `method`, `path`, and `handler`) and returns a new `Handler` that wraps the original. - -This is useful for propagating context like `ThreadLocal` or `ScopedValue`: - -{% capture java %} -config.router.handlerWrapper(endpoint -> ctx -> { - ScopedValue.where(MY_VALUE, "something").run(() -> { - endpoint.handler.handle(ctx); - }); -}); -{% endcapture %} -{% capture kotlin %} -config.router.handlerWrapper { endpoint -> Handler { ctx -> - ScopedValue.where(MY_VALUE, "something").run { - endpoint.handler.handle(ctx) - } -}} -{% endcapture %} -{% include macros/docsSnippet.html java=java kotlin=kotlin %} - -You can also wrap only HTTP endpoints (excluding before/after handlers): - -{% capture java %} -config.router.handlerWrapper(endpoint -> { - if (endpoint.method.isHttpMethod()) { - return ctx -> { - // wrap logic - endpoint.handler.handle(ctx); - }; - } - return endpoint.handler; -}); -{% endcapture %} -{% capture kotlin %} -config.router.handlerWrapper { endpoint -> - if (endpoint.method.isHttpMethod) { - Handler { ctx -> - // wrap logic - endpoint.handler.handle(ctx) - } - } else { - endpoint.handler - } -} -{% endcapture %} -{% include macros/docsSnippet.html java=java kotlin=kotlin %} - ### Context The `Context` object provides you with everything you need to handle a http-request. It contains the underlying servlet-request and servlet-response, and a bunch of getters @@ -403,7 +335,7 @@ async(asyncConfig, runnable) // same as above, but with additonal confi handlerType() // handler type of the current handler (BEFORE, AFTER, GET, etc) appData(typedKey) // get data from the Javalin instance (see app data section below) with(pluginClass) // get context plugin by class, see plugin section below -endpoint().path() // get the path that was used to match this request (ex, "/hello/{name}") +matchedPath() // get the path that was used to match this request (ex, "/hello/{name}") endpointHandlerPath() // get the path of the endpoint handler that was used to match this request cookieStore() // see cookie store section below skipRemainingHandlers() // skip all remaining handlers for this request @@ -493,12 +425,12 @@ Javalin has a very intuitive way of handling WebSockets. You declare an endpoint with a path and configure the different event handlers in a lambda: {% capture java %} -config.routes.ws("/websocket/{path}", ws -> { +app.ws("/websocket/{path}", ws -> { ws.onConnect(ctx -> System.out.println("Connected")); }); {% endcapture %} {% capture kotlin %} -config.routes.ws("/websocket/{path}") { ws -> +app.ws("/websocket/{path}") { ws -> ws.onConnect { ctx -> println("Connected") } } {% endcapture %} @@ -521,30 +453,30 @@ The differences between the different contexts is small, and a full overview can You can learn about how Javalin handles WebSocket concurrency in [FAQ - Concurrency](#concurrency). ### WsBefore -The `config.routes.wsBefore` adds a handler that runs before a WebSocket handler. +The `app.wsBefore` adds a handler that runs before a WebSocket handler. You can have as many before-handlers as you want per WebSocket endpoint, and all events are supported. {% capture java %} -config.routes.wsBefore(ws -> { +app.wsBefore(ws -> { // runs before all WebSocket requests }); -config.routes.wsBefore("/path/*", ws -> { +app.wsBefore("/path/*", ws -> { // runs before websocket requests to /path/* }); {% endcapture %} {% capture kotlin %} -config.routes.wsBefore { ws -> +app.wsBefore { ws -> // runs before all WebSocket requests } -config.routes.wsBefore("/path/*") { ws -> +app.wsBefore("/path/*") { ws -> // runs before websocket requests to /path/* } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} ### WsEndpoint -A WebSocket endpoint is declared with `config.routes.ws(path, handler)`. WebSocket handlers require unique paths. +A WebSocket endpoint is declared with `app.ws(path, handler)`. WebSocket handlers require unique paths. {% capture java %} -config.routes.ws("/websocket/{path}", ws -> { +app.ws("/websocket/{path}", ws -> { ws.onConnect(ctx -> System.out.println("Connected")); ws.onMessage(ctx -> { User user = ctx.messageAsClass(User.class); // convert from json @@ -556,7 +488,7 @@ config.routes.ws("/websocket/{path}", ws -> { }); {% endcapture %} {% capture kotlin %} -config.routes.ws("/websocket/{path}") { ws -> +app.ws("/websocket/{path}") { ws -> ws.onConnect { ctx -> println("Connected") } ws.onMessage { ctx -> val user = ctx.messageAsClass(); // convert from json @@ -570,22 +502,22 @@ config.routes.ws("/websocket/{path}") { ws -> {% include macros/docsSnippet.html java=java kotlin=kotlin %} ### WsAfter -The `config.routes.wsAfter` adds a handler that runs after a WebSocket handler. +The `app.wsAfter` adds a handler that runs after a WebSocket handler. You can have as many after-handlers as you want per WebSocket endpoint, and all events are supported. {% capture java %} -config.routes.wsAfter(ws -> { +app.wsAfter(ws -> { // runs after all WebSocket requests }); -config.routes.wsAfter("/path/*", ws -> { +app.wsAfter("/path/*", ws -> { // runs after websocket requests to /path/* }); {% endcapture %} {% capture kotlin %} -config.routes.wsAfter { ws -> +app.wsAfter { ws -> // runs after all WebSocket requests } -config.routes.wsAfter("/path/*") { ws -> +app.wsAfter("/path/*") { ws -> // runs after websocket requests to /path/* } {% endcapture %} @@ -604,7 +536,7 @@ send(byteBuffer) // send bytes to client sendAsClass(obj, clazz) // serialize object to json string and send it to client // Upgrade Context methods (getters) -endpoint().path() // get the path that was used to match this request (ex, "/hello/{name}") +matchedPath() // get the path that was used to match this request (ex, "/hello/{name}") host() // host as string queryParam("name") // query param by name as string @@ -679,7 +611,7 @@ you can use this safely in multiple locations and from multiple threads. You can import all the HTTP methods with `import static io.javalin.apibuilder.ApiBuilder.*`. {% capture java %} -config.routes.apiBuilder(() -> { +config.router.apiBuilder(() -> { path("/users", () -> { get(UserController::getAllUsers); post(UserController::createUser); @@ -693,7 +625,7 @@ config.routes.apiBuilder(() -> { }); {% endcapture %} {% capture kotlin %} -config.routes.apiBuilder { +config.router.apiBuilder { path("/users") { get(UserController::getAllUsers) post(UserController::createUser) @@ -715,12 +647,12 @@ This means that `path("api", ...)` and `path("/api", ...)` are equivalent. The `CrudHandler` is an interface that can be used within an `apiBuilder()` call: {% capture java %} -config.routes.apiBuilder(() -> { +config.router.apiBuilder(() -> { crud("users/{user-id}", new UserController()); }); {% endcapture %} {% capture kotlin %} -config.routes.apiBuilder { +config.router.apiBuilder { crud("users/{user-id}", UserController()) } {% endcapture %} @@ -855,7 +787,7 @@ val manyErrors = listOf(ageValidator, otherValidator, ...).collectErrors() When a `Validator` throws, it is mapped by: ```kotlin -config.routes.exception(ValidationException::class.java) { e, ctx -> +app.exception(ValidationException::class.java) { e, ctx -> ctx.json(e.errors).status(400) } ``` @@ -863,12 +795,12 @@ config.routes.exception(ValidationException::class.java) { e, ctx -> You can override this by doing: {% capture java %} -config.routes.exception(ValidationException.class, (e, ctx) -> { +app.exception(ValidationException.class, (e, ctx) -> { // your code }); {% endcapture %} {% capture kotlin %} -config.routes.exception(ValidationException::class.java) { e, ctx -> +app.exception(ValidationException::class.java) { e, ctx -> // your code } {% endcapture %} @@ -895,10 +827,10 @@ set per-endpoint authentication and/or authorization. In Javalin 6, this has bee replaced with the `beforeMatched` handler. You can read more about this in the [Javalin 5 to 6 migration guide](/migration-guide-javalin-5-to-6#the-accessmanager-interface-has-been-removed). -To manage access in Javalin 7, you would do something like this: +To manage access in Javalin 6, you would do something like this: {% capture java %} -config.routes.beforeMatched(ctx -> { +app.beforeMatched(ctx -> { var userRole = getUserRole(ctx); // some user defined function that returns a user role if (!ctx.routeRoles().contains(userRole)) { // routeRoles are provided through the Context interface throw new UnauthorizedResponse(); // request will have to be explicitly stopped by throwing an exception @@ -906,7 +838,7 @@ config.routes.beforeMatched(ctx -> { }); {% endcapture %} {% capture kotlin %} -config.routes.beforeMatched { ctx -> +app.beforeMatched { ctx -> val userRole = getUserRole(ctx) // some user defined function that returns a user role if (!ctx.routeRoles().contains(userRole)) { // routeRoles are provided through the Context interface throw UnauthorizedResponse() // request will have to be explicitly stopped by throwing an exception @@ -918,12 +850,12 @@ config.routes.beforeMatched { ctx -> The roles are set when you declare your endpoints: {% capture java %} -config.routes.get("/public", ctx -> ctx.result("Hello public"), Role.OPEN); -config.routes.get("/private", ctx -> ctx.result("Hello private"), Role.LOGGED_IN); +app.get("/public", ctx -> ctx.result("Hello public"), Role.OPEN); +app.get("/private", ctx -> ctx.result("Hello private"), Role.LOGGED_IN); {% endcapture %} {% capture kotlin %} -config.routes.get("/public", { ctx -> ctx.result("Hello public") }, Role.OPEN) -config.routes.get("/private", { ctx -> ctx.result("Hello private") }, Role.LOGGED_IN) +app.get("/public", { ctx -> ctx.result("Hello public") }, Role.OPEN) +app.get("/private", { ctx -> ctx.result("Hello private") }, Role.LOGGED_IN) {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -932,7 +864,7 @@ Javalin comes with a built in class called `HttpResponseException`, which can be If the client accepts JSON, a JSON object is returned. Otherwise a plain text response is returned. ```java -config.routes.post("/", ctx -> { throw new ForbiddenResponse("Off limits!"); }); +app.post("/") { throw new ForbiddenResponse("Off limits!") } ``` If client accepts JSON: ```java @@ -989,51 +921,47 @@ Returns a [504 Gateway Timeout](https://developer.mozilla.org/en-US/docs/Web/HTT ## Exception Mapping All handlers (before, endpoint, after, ws) can throw `Exception` (and any subclass of `Exception`). -The `config.routes.exception()` and `config.routes.wsException()` methods give you a way of handling these exceptions: +The `app.exception()` and `app.wsException()` methods gives you a way of handling these exceptions: {% capture java %} -Javalin.create(config -> { - // HTTP exceptions - config.routes.exception(NullPointerException.class, (e, ctx) -> { - // handle nullpointers here - }); +// HTTP exceptions +app.exception(NullPointerException.class, (e, ctx) -> { + // handle nullpointers here +}); - config.routes.exception(Exception.class, (e, ctx) -> { - // handle general exceptions here - // will not trigger if more specific exception-mapper found - }); +app.exception(Exception.class, (e, ctx) -> { + // handle general exceptions here + // will not trigger if more specific exception-mapper found +}); - // WebSocket exceptions - config.routes.wsException(NullPointerException.class, (e, ctx) -> { - // handle nullpointers here - }); +// WebSocket exceptions +app.wsException(NullPointerException.class, (e, ctx) -> { + // handle nullpointers here +}); - config.routes.wsException(Exception.class, (e, ctx) -> { - // handle general exceptions here - // will not trigger if more specific exception-mapper found - }); +app.wsException(Exception.class, (e, ctx) -> { + // handle general exceptions here + // will not trigger if more specific exception-mapper found }); {% endcapture %} {% capture kotlin %} -Javalin.create { config -> - // HTTP exceptions - config.routes.exception(NullPointerException::class.java) { e, ctx -> - // handle nullpointers here - } +// HTTP exceptions +app.exception(NullPointerException::class.java) { e, ctx -> + // handle nullpointers here +} - config.routes.exception(Exception::class.java) { e, ctx -> - // handle general exceptions here - // will not trigger if more specific exception-mapper found - } +app.exception(Exception::class.java) { e, ctx -> + // handle general exceptions here + // will not trigger if more specific exception-mapper found +} - // WebSocket exceptions - config.routes.wsException(NullPointerException::class.java) { e, ctx -> - // handle nullpointers here - } +// WebSocket exceptions +app.wsException(NullPointerException::class.java) { e, ctx -> + // handle nullpointers here +} - config.routes.wsException(Exception::class.java) { e, ctx -> - // handle general exceptions here - // will not trigger if more specific exception-mapper found - } +app.wsException(Exception::class.java) { e, ctx -> + // handle general exceptions here + // will not trigger if more specific exception-mapper found } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -1041,17 +969,13 @@ Javalin.create { config -> ## Error Mapping HTTP Error mapping is similar to exception mapping, but it operates on HTTP status codes instead of Exceptions: {% capture java %} -Javalin.create(config -> { - config.routes.error(404, ctx -> { - ctx.result("Generic 404 message"); - }); +app.error(404, ctx -> { + ctx.result("Generic 404 message"); }); {% endcapture %} {% capture kotlin %} -Javalin.create { config -> - config.routes.error(404) { ctx -> - ctx.result("Generic 404 message") - } +app.error(404) { ctx -> + ctx.result("Generic 404 message") } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -1059,23 +983,17 @@ Javalin.create { config -> It can make sense to use them together: {% capture java %} -Javalin.create(config -> { - config.routes.exception(FileNotFoundException.class, (e, ctx) -> { - ctx.status(404); - }); - config.routes.error(404, ctx -> { - ctx.result("Generic 404 message"); - }); +app.exception(FileNotFoundException.class, (e, ctx) -> { + ctx.status(404); +}).error(404, ctx -> { + ctx.result("Generic 404 message"); }); {% endcapture %} {% capture kotlin %} -Javalin.create { config -> - config.routes.exception(FileNotFoundException::class.java) { e, ctx -> - ctx.status(404) - } - config.routes.error(404) { ctx -> - ctx.result("Generic 404 message") - } +app.exception(FileNotFoundException::class.java) { e, ctx -> + ctx.status(404) +}.error(404) { ctx -> + ctx.result("Generic 404 message") } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -1083,17 +1001,13 @@ Javalin.create { config -> You can also include the content type when declaring your error mappers: {% capture java %} -Javalin.create(config -> { - config.routes.error(404, "html", ctx -> { - ctx.html("Generic 404 message"); - }); +app.error(404, "html", ctx -> { + ctx.html("Generic 404 message"); }); {% endcapture %} {% capture kotlin %} -Javalin.create { config -> - config.routes.error(404, "html") { ctx -> - ctx.html("Generic 404 message") - } +app.error(404, "html") { ctx -> + ctx.html("Generic 404 message") } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -1102,17 +1016,17 @@ This can be useful if you, for example, want one set of error handlers for HTML, ## Server-sent Events Server-sent events (often also called event source) are very simple in Javalin. -You call `config.routes.sse()`, which gives you access to the connected `SseClient`: +You call `app.sse()`, which gives you access to the connected `SseClient`: {% capture java %} -config.routes.sse("/sse", client -> { +app.sse("/sse", client -> { client.sendEvent("connected", "Hello, SSE"); client.onClose(() -> System.out.println("Client disconnected")); client.close(); // close the client }); {% endcapture %} {% capture kotlin %} -config.routes.sse("/sse") { client -> +app.sse("/sse") { client -> client.sendEvent("connected", "Hello, SSE") client.onClose { println("Client disconnected") } client.close() // close the client @@ -1125,7 +1039,7 @@ Clients are automatically closed when leaving the handler, if you need to use th {% capture java %} Queue clients = new ConcurrentLinkedQueue(); -config.routes.sse("/sse", client -> { +app.sse("/sse", client -> { client.keepAlive(); client.onClose(() - > clients.remove(client)); clients.add(client); @@ -1134,7 +1048,7 @@ config.routes.sse("/sse", client -> { {% capture kotlin %} val clients = ConcurrentLinkedQueue() -config.routes.sse("/sse") { client -> +app.sse("/sse") { client -> client.keepAlive() client.onClose { clients.remove(client) } clients.add(client) @@ -1177,14 +1091,13 @@ Javalin.create(config -> { config.useVirtualThreads // Use virtual threads (based on Java Project Loom) config.showJavalinBanner // Show the Javalin banner in the logs config.startupWatcherEnabled // Print warning if instance was not started after 5 seconds + config.pvt // This is "private", only use it if you know what you're doing config.events(listenerConfig) // Add an event listener config.jsonMapper(jsonMapper) // Set a custom JsonMapper config.fileRenderer(fileRenderer) // Set a custom FileRenderer config.registerPlugin(plugin) // Register a plugin config.appData(key, data) // Store data on the Javalin instance - - config.unsafe // Advanced/unsafe API providing access to internal Javalin configuration (use with caution) }); ``` @@ -1253,8 +1166,8 @@ Javalin.create { config -> ### JettyConfig {% capture java %} Javalin.create(config -> { - config.jetty.host = "localhost"; // set the host for Jetty - config.jetty.port = 1234; // set the port for Jetty + config.jetty.defaultHost = "localhost"; // set the default host for Jetty + config.jetty.defaultPort = 1234; // set the default port for Jetty config.jetty.threadPool = new ThreadPool(); // set the thread pool for Jetty config.jetty.timeoutStatus = 408; // set the timeout status for Jetty (default 500) config.jetty.clientAbortStatus = 499; // set the abort status for Jetty (default 500) @@ -1268,8 +1181,8 @@ Javalin.create(config -> { {% endcapture %} {% capture kotlin %} Javalin.create { config -> - config.jetty.host = "localhost" // set the host for Jetty - config.jetty.port = 1234 // set the port for Jetty + config.jetty.defaultHost = "localhost" // set the default host for Jetty + config.jetty.defaultPort = 1234 // set the default port for Jetty config.jetty.threadPool = ThreadPool() // set the thread pool for Jetty config.jetty.timeoutStatus = 408 // set the timeout status for Jetty (default 500) config.jetty.clientAbortStatus = 499 // set the abort status for Jetty (default 500) @@ -1329,7 +1242,7 @@ Javalin.create { config -> {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} -You can add a WebSocket logger by calling `config.requestLogger.ws()`. The method takes a the same arguments as a normal `config.routes.ws()` call, +You can add a WebSocket logger by calling `config.requestLogger.ws()`. The method takes a the same arguments as a normal `app.ws()` call, and can be used to log events of all types. The following example just shows `onMessage`, but `onConnect`, `onError` and `onClose` are all available: @@ -1447,25 +1360,6 @@ they will be available at `/webjars/name/version/file.ext`. WebJars can be found on [https://www.webjars.org/](https://www.webjars.org/). Everything available through NPM is also available through WebJars. -#### Jetty-free static file serving -If you want to serve static files without depending on Jetty's resource handling, -you can use the `JavalinStaticResourceHandler`: - -{% capture java %} -Javalin.create(config -> { - config.resourceHandler(new JavalinStaticResourceHandler()); -}); -{% endcapture %} -{% capture kotlin %} -Javalin.create { config -> - config.resourceHandler(JavalinStaticResourceHandler()) -} -{% endcapture %} -{% include macros/docsSnippet.html java=java kotlin=kotlin %} - -This is useful if you want to use Javalin with a different embedded server, -or if you want to avoid Jetty-specific dependencies for static file handling. - If you are building a Single Page Application (SPA), you should have a look at the [SpaRootConfig](#sparootconfig) @@ -1488,11 +1382,8 @@ dependency to your project: ### Server setup -Javalin runs on an embedded [Jetty](http://eclipse.org/jetty/). - -**Note:** Javalin 7 uses **Jetty 12** (previously Jetty 11), which means servlet packages have changed from `javax.servlet.*` to `jakarta.servlet.*`. - -To start and stop the server, use `start()` and `stop`: +Javalin runs on an embedded [Jetty](http://eclipse.org/jetty/). To start and stop the server, +use `start()` and `stop`: ```java Javalin app = Javalin.create() @@ -1500,36 +1391,20 @@ Javalin app = Javalin.create() .stop() // stop server (sync/blocking) ``` -There is also a convenience method `Javalin.start(config)` that creates and starts a Javalin instance in one call: - -{% capture java %} -Javalin app = Javalin.start(config -> { - config.jetty.port = 8080; - config.routes.get("/", ctx -> ctx.result("Hello World")); -}); -{% endcapture %} -{% capture kotlin %} -val app = Javalin.start { config -> - config.jetty.port = 8080 - config.routes.get("/") { ctx -> ctx.result("Hello World") } -} -{% endcapture %} -{% include macros/docsSnippet.html java=java kotlin=kotlin %} - The `app.start()` method spawns a user thread, starts the server, and then returns. Your program will not exit until this thread is terminated by calling `app.stop()`. If you want to do a clean shutdown when the program is exiting, you could use: ```java -Javalin app = Javalin.create(config -> { - config.events.serverStopping(() -> { /* Your code here */ }); - config.events.serverStopped(() -> { /* Your code here */ }); -}); - Runtime.getRuntime().addShutdownHook(new Thread(() -> { app.stop(); })); + +app.events(event -> { + event.serverStopping(() -> { /* Your code here */ }); + event.serverStopped(() -> { /* Your code here */ }); +}); ``` If you want graceful shutdown, you can configure the server using the `modifyServer` method: @@ -1602,28 +1477,28 @@ fully functioning example server in both Kotlin and Java: [javalin-http2-example Javalin has events for server start/stop, as well as for when handlers are added. The snippet below shows all of them in action: {% capture java %} -Javalin app = Javalin.create(config -> { - config.events.serverStarting(() -> { ... }); - config.events.serverStarted(() -> { ... }); - config.events.serverStartFailed(() -> { ... }); - config.events.serverStopping(() -> { ... }); - config.events.serverStopped(() -> { ... }); - config.events.handlerAdded(handlerMetaInfo -> { ... }); - config.events.wsHandlerAdded(wsHandlerMetaInfo -> { ... }); +Javalin app = Javalin.create().events(event -> { + event.serverStarting(() -> { ... }); + event.serverStarted(() -> { ... }); + event.serverStartFailed(() -> { ... }); + event.serverStopping(() -> { ... }); + event.serverStopped(() -> { ... }); + event.handlerAdded(handlerMetaInfo -> { ... }); + event.wsHandlerAdded(wsHandlerMetaInfo -> { ... }); }); app.start() // serverStarting -> (serverStarted || serverStartFailed) app.stop() // serverStopping -> serverStopped {% endcapture %} {% capture kotlin %} -Javalin app = Javalin.create { config -> - config.events.serverStarting { ... } - config.events.serverStarted { ... } - config.events.serverStartFailed { ... } - config.events.serverStopping { ... } - config.events.serverStopped { ... } - config.events.handlerAdded { handlerMetaInfo -> } - config.events.wsHandlerAdded { wsHandlerMetaInfo -> } +Javalin app = Javalin.create().events { event -> + event.serverStarting { ... } + event.serverStarted { ... } + event.serverStartFailed { ... } + event.serverStopping { ... } + event.serverStopped { ... } + event.handlerAdded { handlerMetaInfo -> } + event.wsHandlerAdded { wsHandlerMetaInfo -> } } app.start() // serverStarting -> (serverStarted || serverStartFailed) @@ -1644,12 +1519,13 @@ Frequently asked questions. The Javalin request lifecycle is pretty straightforward. The following snippet covers every place you can hook into: ```java -config.routes.before // runs first, can throw exception (which will skip any endpoint handlers) -config.routes.get/post/patch/etc // runs second, can throw exception -config.routes.error // runs third, can throw exception -config.routes.after // runs fourth, can throw exception -config.routes.exception // runs any time a handler throws (cannot throw exception) -JavalinConfig#requestLogger // runs after response is written to client +Javalin#before // runs first, can throw exception (which will skip any endpoint handlers) +Javalin#get/post/patch/etc // runs second, can throw exception +Javalin#error // runs third, can throw exception +Javalin#after // runs fourth, can throw exception +Javalin#exception // runs any time a handler throws (cannot throw exception) +JavalinConfig#requestLogger // runs after response is written to client +JavalinConfig#accessManager // wraps all your endpoint handlers in a lambda of your choice ``` --- @@ -1659,7 +1535,7 @@ There is a very simple rate limiter included in Javalin. You can call it in the beginning of your endpoint `Handler` functions: {% capture java %} -config.routes.get("/", ctx -> { +app.get("/", ctx -> { NaiveRateLimit.requestPerTimeUnit(ctx, 5, TimeUnit.MINUTES); // throws if rate limit is exceeded ctx.status("Hello, rate-limited World!"); }); @@ -1668,7 +1544,7 @@ config.routes.get("/", ctx -> { RateLimitUtil.keyFunction = ctx -> // uses (ip+method+endpointPath) by default {% endcapture %} {% capture kotlin %} -config.routes.get("/") { ctx -> +app.get("/") { ctx -> NaiveRateLimit.requestPerTimeUnit(ctx, 5, TimeUnit.MINUTES) // throws if rate limit is exceeded ctx.status("Hello, rate-limited World!") } @@ -1770,18 +1646,17 @@ on another web server (such as Tomcat), you can use Maven or Gradle to exclude J ### Uploads Uploaded files are easily accessible via `ctx.uploadedFiles()`: {% capture java %} -config.routes.post("/upload", ctx -> { +app.post("/upload", ctx -> { ctx.uploadedFiles("files").forEach(uploadedFile -> FileUtil.streamToFile(uploadedFile.content(), "upload/" + uploadedFile.filename())); }); {% endcapture %} {% capture kotlin %} -config.routes.post("/upload") { ctx -> +app.post("/upload") { ctx -> ctx.uploadedFiles("files").forEach { uploadedFile -> FileUtil.streamToFile(uploadedFile.content(), "upload/${uploadedFile.filename()}") } } -} {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -1828,7 +1703,7 @@ private fun getRandomCatFactFuture(): CompletableFuture> { Now we can use this method in our Javalin app to return cat facts to the client asynchronously: {% capture java %} -config.routes.get("/cat-facts", ctx -> { +app.get("/cat-facts", ctx -> { ctx.future(() -> { return getRandomCatFactFuture() .thenAccept(response -> ctx.html(response.body()).status(response.statusCode())) @@ -1840,7 +1715,7 @@ config.routes.get("/cat-facts", ctx -> { }); {% endcapture %} {% capture kotlin %} -config.routes.get("/cat-facts") { ctx -> +app.get("/cat-facts") { ctx -> ctx.future { getRandomCatFactFuture() .thenAccept { response -> ctx.html(response.body()).status(response.statusCode()) } @@ -1878,7 +1753,7 @@ once the task is done. The snippet belows shows a full example with a custom timeout, timeout handler, and a task: {% capture java %} -config.routes.get("/async", ctx -> { +app.get("/async", ctx -> { ctx.async( 1000, // timeout in ms () -> ctx.result("Request took too long"), // timeout callback @@ -1887,7 +1762,8 @@ config.routes.get("/async", ctx -> { }); {% endcapture %} {% capture kotlin %} -config.routes.get("/async") { ctx -> + +app.get("/async") { ctx -> ctx.async( 1000, // timeout in ms { ctx.result("Request took too long") }, // timeout callback @@ -2028,24 +1904,6 @@ about this at [/plugins/rendering](/plugins/rendering). ### Vue support (JavalinVue) If you don't want to deal with NPM and frontend builds, Javalin has support for simplified Vue.js development. - -**Note:** In Javalin 7, JavalinVue is now a plugin. You need to register it using `config.registerPlugin(new JavalinVuePlugin(...))`. - -**Note:** The `LoadableData` JavaScript class is no longer included by default. -If you want to use `LoadableData`, you need to explicitly enable it: - -{% capture java %} -config.registerPlugin(new JavalinVuePlugin(vue -> { - vue.enableLoadableData = true; -})); -{% endcapture %} -{% capture kotlin %} -config.registerPlugin(JavalinVuePlugin { vue -> - vue.enableLoadableData = true -}) -{% endcapture %} -{% include macros/docsSnippet.html java=java kotlin=kotlin %} - This requires you to make a layout template, `src/main/resources/vue/layout.html`: ```markup @@ -2105,19 +1963,19 @@ which is 30 seconds by default in Jetty/Javalin. This is not a bug. ### Java lang Error handling Javalin has a default error handler for `java.lang.Error` that will log the error and return a 500. -The default error handler can be overridden using `config.router`: +The default error handler can be overridden using the private config: {% capture java %} -Javalin.create(config -> { - config.router.javaLangErrorHandler((res, error) -> { +Javalin.create( cfg -> { + cfg.pvt.javaLangErrorHandler((res, error) -> { res.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.getCode()); JavalinLogger.error("Exception occurred while servicing http-request", error); }); }); {% endcapture %} {% capture kotlin %} -Javalin.create { config -> - config.router.javaLangErrorHandler { res, error -> +Javalin.create { cfg -> + cfg.pvt.javaLangErrorHandler { res, error -> res.status = HttpStatus.INTERNAL_SERVER_ERROR.code JavalinLogger.error("Exception occurred while servicing http-request", error) } diff --git a/pages/docs/docs.md b/pages/docs/docs.md index d2c8dbe..916d4e7 100644 --- a/pages/docs/docs.md +++ b/pages/docs/docs.md @@ -3,6 +3,7 @@ layout: default title: Documentation rightmenu: true permalink: /documentation +description: "Javalin v7 documentation. Full reference for routing, handlers, WebSockets, validation, SSE, and configuration. Java 17+, Jetty 12." --- {% include notificationBanner.html %} @@ -13,11 +14,14 @@ permalink: /documentation - [Before](#before-handlers) - [Endpoint](#endpoint-handlers) - [After](#after-handlers) + - [Wrapper](#wrapper-handlers) - [Context (ctx)](#context) - [WebSockets](#websockets) - [Before](#wsbefore) - [Endpoint](#wsendpoint) - [After](#wsafter) + - [BeforeUpgrade](#wsbeforeupgrade) + - [AfterUpgrade](#wsafterupgrade) - [WsContext (wsCtx)](#wscontext) - [Handler groups](#handler-groups) - [Validation](#validation) @@ -40,6 +44,7 @@ permalink: /documentation - [RouterConfig](#routerconfig) - [SpaRootConfig](#sparootconfig) - [StaticFileConfig](#staticfileconfig) + - [BundledPluginsConfig](#bundledpluginsconfig) - [Logging](#logging) - [Server setup](#server-setup) - [Lifecycle events](#lifecycle-events) @@ -67,9 +72,8 @@ permalink: /documentation

Documentation

-The documentation is for the latest version of Javalin, currently `{{site.javalinversion}}`. -Javalin follows [semantic versioning](http://semver.org/), meaning there are no breaking -changes unless the major (leftmost) digit changes, for example `5.X.X` to `6.X.X`. +Javalin 7 requires **Java 17+**, and **Jetty 12+**. +See the [migration guide](/migration-guide-javalin-6-to-7) for details on upgrading from Javalin 6. Javalin follows [semantic versioning](http://semver.org/), meaning there are no breaking changes unless the major (leftmost) digit changes, for example `6.X.X` to `7.X.X`. {% include sponsorOrStar.html %} @@ -81,12 +85,17 @@ Add the dependency: Start coding: {% include macros/gettingStarted.md %} +
+**Important change in Javalin 7:** Routes must now be defined in the `config.routes` block during application creation. +You can no longer add routes after calling `.start()`. See the [migration guide](/migration-guide-javalin-6-to-7) for details. +
+ ## Handlers Javalin has three main handler types: before-handlers, endpoint-handlers, and after-handlers. (There are also exception-handlers and error-handlers, but we'll get to them later). The before-, endpoint- and after-handlers require three parts: -* A verb, one of: `before`, `get`, `post`, `put`, `patch`, `delete`, `after` (... `head`, `options`, `trace`, `connect`) +* A verb, one of: `before`, `get`, `post`, `query`, `put`, `patch`, `delete`, `after` (... `head`, `options`, `trace`, `connect`) * A path, ex: `/`, `/hello-world`, `/hello/{name}` * A handler implementation, ex `ctx -> { ... }`, `MyClass implements Handler`, etc @@ -103,33 +112,33 @@ Before-handlers are matched before every request (including static files).
You might know before-handlers as filters, interceptors, or middleware from other libraries.
{% capture java %} -app.before(ctx -> { +config.routes.before(ctx -> { // runs before all requests }); -app.before("/path/*", ctx -> { +config.routes.before("/path/*", ctx -> { // runs before request to /path/* }); {% endcapture %} {% capture kotlin %} -app.before { ctx -> +config.routes.before { ctx -> // runs before all requests } -app.before("/path/*") { ctx -> +config.routes.before("/path/*") { ctx -> // runs before request to /path/* } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} In some cases, you might want to only run a before-handler if the request will be matched (not 404). -In this case you can use the `app.beforeMatched` method: +In this case you can use the `config.routes.beforeMatched` method: {% capture java %} -app.beforeMatched(ctx -> { +config.routes.beforeMatched(ctx -> { // runs before all matched requests (including static files) }); {% endcapture %} {% capture kotlin %} -app.beforeMatched { ctx -> +config.routes.beforeMatched { ctx -> // runs before all matched requests (including static files) } {% endcapture %} @@ -139,31 +148,34 @@ app.beforeMatched { ctx -> ### Endpoint handlers Endpoint handlers are the main handler type, and defines your API. You can add a GET handler to serve data to a client, or a POST handler to receive some data. -Common methods are supported directly on the `Javalin` class (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS), -uncommon operations (TRACE, CONNECT) are supported via `Javalin#addHandler`. +Common methods are supported via `config.routes` (GET, POST, QUERY, PUT, PATCH, DELETE, HEAD, OPTIONS), +uncommon operations (TRACE, CONNECT) are supported via `config.routes.addHttpHandler`. + +The [QUERY method](https://httpwg.org/http-extensions/draft-ietf-httpbis-safe-method-w-body.html) is similar to GET, +but allows a request body. This is useful for complex queries that don't fit in a URL. Endpoint-handlers are matched in the order they are defined.
You might know endpoint-handlers as routes or middleware from other libraries.
{% capture java %} -app.get("/output", ctx -> { +config.routes.get("/output", ctx -> { // some code ctx.json(object); }); -app.post("/input", ctx -> { +config.routes.post("/input", ctx -> { // some code ctx.status(201); }); {% endcapture %} {% capture kotlin %} -app.get("/output") { ctx -> +config.routes.get("/output") { ctx -> // some code ctx.json(object) } -app.post("/input") { ctx -> +config.routes.post("/input") { ctx -> // some code ctx.status(201) } @@ -172,18 +184,18 @@ app.post("/input") { ctx -> Handler paths can include path-parameters. These are available via `ctx.pathParam("key")`: {% capture java %} -app.get("/hello/{name}", ctx -> { // the {} syntax does not allow slashes ('/') as part of the parameter +config.routes.get("/hello/{name}", ctx -> { // the {} syntax does not allow slashes ('/') as part of the parameter ctx.result("Hello: " + ctx.pathParam("name")); }); -app.get("/hello/", ctx -> { // the <> syntax allows slashes ('/') as part of the parameter +config.routes.get("/hello/", ctx -> { // the <> syntax allows slashes ('/') as part of the parameter ctx.result("Hello: " + ctx.pathParam("name")); }); {% endcapture %} {% capture kotlin %} -app.get("/hello/{name}") { ctx -> // the {} syntax does not allow slashes ('/') as part of the parameter +config.routes.get("/hello/{name}") { ctx -> // the {} syntax does not allow slashes ('/') as part of the parameter ctx.result("Hello: " + ctx.pathParam("name")) } -app.get("/hello/") { ctx -> // the <> syntax allows slashes ('/') as part of the parameter +config.routes.get("/hello/") { ctx -> // the <> syntax allows slashes ('/') as part of the parameter ctx.result("Hello: " + ctx.pathParam("name")) } {% endcapture %} @@ -192,13 +204,13 @@ app.get("/hello/") { ctx -> // the <> syntax allows slashes ('/') as part Handler paths can also include wildcard parameters: {% capture java %} -app.get("/path/*", ctx -> { // will match anything starting with /path/ - ctx.result("You are here because " + ctx.path() + " matches " + ctx.matchedPath()); +config.routes.get("/path/*", ctx -> { // will match anything starting with /path/ + ctx.result("You are here because " + ctx.path() + " matches " + ctx.endpoint().path); }); {% endcapture %} {% capture kotlin %} -app.get("/path/*") { ctx -> // will match anything starting with /path/ - ctx.result("You are here because " + ctx.path() + " matches " + ctx.matchedPath()) +config.routes.get("/path/*") { ctx -> // will match anything starting with /path/ + ctx.result("You are here because " + ctx.path() + " matches " + ctx.endpoint().path) } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -206,43 +218,120 @@ app.get("/path/*") { ctx -> // will match anything starting with /path/ However, you cannot extract the value of a wildcard. Use a slash accepting path-parameter (`` instead of `{param-name}`) if you need this behavior. +#### Custom HTTP methods +In Javalin 7, `HandlerType` is a record instead of an enum, which means you can create custom HTTP methods +dynamically using `HandlerType.findOrCreate("METHOD_NAME")`. This is useful for protocols like WebDAV +that use non-standard HTTP methods such as `PROPFIND`, `MKCOL`, `LOCK`, etc. + +{% capture java %} +HandlerType PROPFIND = HandlerType.findOrCreate("PROPFIND"); + +Javalin.create(config -> { + config.routes.addHttpHandler(PROPFIND, "/webdav/{resource}", ctx -> { + ctx.result("PROPFIND: " + ctx.pathParam("resource")); + }); +}); +{% endcapture %} +{% capture kotlin %} +val PROPFIND = HandlerType.findOrCreate("PROPFIND") + +Javalin.create { config -> + config.routes.addHttpHandler(PROPFIND, "/webdav/{resource}") { ctx -> + ctx.result("PROPFIND: " + ctx.pathParam("resource")) + } +} +{% endcapture %} +{% include macros/docsSnippet.html java=java kotlin=kotlin %} + +Custom methods work with all Javalin features including path parameters, roles, before/after handlers, and exception handling. +Method names must consist of uppercase letters only (e.g. `PROPFIND`, `MKCOL`). + ### After handlers After-handlers run after every request (even if an exception occurred)
You might know after-handlers as filters, interceptors, or middleware from other libraries.
{% capture java %} -app.after(ctx -> { +config.routes.after(ctx -> { // run after all requests }); -app.after("/path/*", ctx -> { +config.routes.after("/path/*", ctx -> { // runs after request to /path/* }); {% endcapture %} {% capture kotlin %} -app.after { ctx -> +config.routes.after { ctx -> // run after all requests } -app.after("/path/*") { ctx -> +config.routes.after("/path/*") { ctx -> // runs after request to /path/* } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} In some cases, you might want to only run an after-handler if the request will be matched (not 404). -In this case you can use the `app.afterMatched` method: +In this case you can use the `config.routes.afterMatched` method: {% capture java %} -app.afterMatched(ctx -> { +config.routes.afterMatched(ctx -> { // runs after all matched requests (including static files) }); {% endcapture %} {% capture kotlin %} -app.afterMatched { ctx -> +config.routes.afterMatched { ctx -> // runs after all matched requests (including static files) } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} +### Wrapper handlers +Wrapper-handlers run "around" endpoint handlers. The `HandlerWrapper` functional interface +receives an `Endpoint` (with `method`, `path`, and `handler`) and returns a new `Handler` that wraps the original. + +This is useful for propagating context like `ThreadLocal` or `ScopedValue`: + +{% capture java %} +config.router.handlerWrapper(endpoint -> ctx -> { + ScopedValue.where(MY_VALUE, "something").run(() -> { + endpoint.handler.handle(ctx); + }); +}); +{% endcapture %} +{% capture kotlin %} +config.router.handlerWrapper { endpoint -> Handler { ctx -> + ScopedValue.where(MY_VALUE, "something").run { + endpoint.handler.handle(ctx) + } +}} +{% endcapture %} +{% include macros/docsSnippet.html java=java kotlin=kotlin %} + +You can also wrap only HTTP endpoints (excluding before/after handlers): + +{% capture java %} +config.router.handlerWrapper(endpoint -> { + if (endpoint.method.isHttpMethod()) { + return ctx -> { + // wrap logic + endpoint.handler.handle(ctx); + }; + } + return endpoint.handler; +}); +{% endcapture %} +{% capture kotlin %} +config.router.handlerWrapper { endpoint -> + if (endpoint.method.isHttpMethod) { + Handler { ctx -> + // wrap logic + endpoint.handler.handle(ctx) + } + } else { + endpoint.handler + } +} +{% endcapture %} +{% include macros/docsSnippet.html java=java kotlin=kotlin %} + ### Context The `Context` object provides you with everything you need to handle a http-request. It contains the underlying servlet-request and servlet-response, and a bunch of getters @@ -255,7 +344,7 @@ bodyAsBytes() // request body as array of bytes bodyAsClass(clazz) // request body as specified class (deserialized from JSON) bodyStreamAsClass(clazz) // request body as specified class (memory optimized version of above) bodyValidator(clazz) // request body as validator typed as specified class -bodyInputStream() // the underyling input stream of the request +bodyInputStream() // the underlying input stream of the request uploadedFile("name") // uploaded file by name uploadedFiles("name") // all uploaded files by name uploadedFiles() // all uploaded files as list @@ -312,31 +401,39 @@ result("result") // set result stream to specified string ( result(byteArray) // set result stream to specified byte array (overwrites any previously set result) result(inputStream) // set result stream to specified input stream (overwrites any previously set result) future(futureSupplier) // set the result to be a future, see async section (overwrites any previously set result) -writeSeekableStream(inputStream) // write content immediately as seekable stream (useful for audio and video) +writeSeekableStream(stream, type) // write content immediately as seekable stream (useful for audio and video) result() // get current result stream as string (if possible), and reset result stream resultInputStream() // get current result stream contentType("type") // set the response content type header("name", "value") // set response header by name (can be used with Header.HEADERNAME) +removeHeader("name") // remove a response header by name redirect("/path", code) // redirect to the given path with the given status code status(code) // set the response status code -status() // get the response status code +status() // get the response status +statusCode() // get the response status code as int cookie("name", "value", maxAge) // set response cookie by name, with value and max-age (optional). cookie(cookie) // set cookie using javalin Cookie class removeCookie("name", "/path") // removes cookie by name and path (optional) json(obj) // calls result(jsonString), and also sets content type to json jsonStream(obj) // calls result(jsonStream), and also sets content type to json +writeJsonStream(stream) // writes JSON stream directly to response (memory efficient for large collections) html("html") // calls result(string), and also sets content type to html render("/template.tmpl", model) // calls html(renderedTemplate) res() // get the underlying HttpServletResponse // Other methods async(runnable) // lifts request out of Jetty's ThreadPool, and moves it to Javalin's AsyncThreadPool -async(asyncConfig, runnable) // same as above, but with additonal config -handlerType() // handler type of the current handler (BEFORE, AFTER, GET, etc) +async(asyncConfig, runnable) // same as above, but with additional config +endpoint() // get the current Endpoint (method, path, handler) +endpoint().method // handler type of the current handler (BEFORE, AFTER, GET, etc) +endpoint().path // get the path that was used to match this request (ex, "/hello/{name}") +endpoints() // get all endpoints visited during this request (in order) +endpoints().current() // get the last endpoint in the stack +endpoints().lastHttpEndpoint() // get the last HTTP endpoint (useful in AFTER handlers) +endpoints().list() // get all endpoints as an unmodifiable list +routeRoles() // get the roles for the matched endpoint appData(typedKey) // get data from the Javalin instance (see app data section below) with(pluginClass) // get context plugin by class, see plugin section below -matchedPath() // get the path that was used to match this request (ex, "/hello/{name}") -endpointHandlerPath() // get the path of the endpoint handler that was used to match this request cookieStore() // see cookie store section below skipRemainingHandlers() // skip all remaining handlers for this request ``` @@ -389,27 +486,35 @@ The cookieStore works like this: ##### Example: {% capture java %} -serverOneApp.post("/cookie-storer", ctx -> { - ctx.cookieStore().set("string", "Hello world!"); - ctx.cookieStore().set("i", 42); - ctx.cookieStore().set("list", Arrays.asList("One", "Two", "Three")); +var serverOneApp = Javalin.create(config -> { + config.routes.post("/cookie-storer", ctx -> { + ctx.cookieStore().set("string", "Hello world!"); + ctx.cookieStore().set("i", 42); + ctx.cookieStore().set("list", Arrays.asList("One", "Two", "Three")); + }); }); -serverTwoApp.get("/cookie-reader", ctx -> { // runs on a different server than serverOneApp - String string = ctx.cookieStore().get("string") - int i = ctx.cookieStore().get("i") - List list = ctx.cookieStore().get("list") +var serverTwoApp = Javalin.create(config -> { + config.routes.get("/cookie-reader", ctx -> { // runs on a different server than serverOneApp + String string = ctx.cookieStore().get("string"); + int i = ctx.cookieStore().get("i"); + List list = ctx.cookieStore().get("list"); + }); }); {% endcapture %} {% capture kotlin %} -serverOneApp.post("/cookie-storer") { ctx -> - ctx.cookieStore().set("string", "Hello world!") - ctx.cookieStore().set("i", 42) - ctx.cookieStore().set("list", listOf("One", "Two", "Three")) +val serverOneApp = Javalin.create { config -> + config.routes.post("/cookie-storer") { ctx -> + ctx.cookieStore().set("string", "Hello world!") + ctx.cookieStore().set("i", 42) + ctx.cookieStore().set("list", listOf("One", "Two", "Three")) + } } -serverTwoApp.get("/cookie-reader") { ctx -> // runs on a different server than serverOneApp - val string = ctx.cookieStore().get("string") - val i = ctx.cookieStore().get("i") - val list = ctx.cookieStore().get("list") +val serverTwoApp = Javalin.create { config -> + config.routes.get("/cookie-reader") { ctx -> // runs on a different server than serverOneApp + val string = ctx.cookieStore().get("string") + val i = ctx.cookieStore().get("i") + val list = ctx.cookieStore().get("list") + } } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -425,18 +530,18 @@ Javalin has a very intuitive way of handling WebSockets. You declare an endpoint with a path and configure the different event handlers in a lambda: {% capture java %} -app.ws("/websocket/{path}", ws -> { +config.routes.ws("/websocket/{path}", ws -> { ws.onConnect(ctx -> System.out.println("Connected")); }); {% endcapture %} {% capture kotlin %} -app.ws("/websocket/{path}") { ws -> +config.routes.ws("/websocket/{path}") { ws -> ws.onConnect { ctx -> println("Connected") } } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} -There are a total of five events supported: +There are a total of six events supported: ```java ws.onConnect(WsConnectContext) @@ -444,6 +549,7 @@ ws.onError(WsErrorContext) ws.onClose(WsCloseContext) ws.onMessage(WsMessageContext) ws.onBinaryMessage(WsBinaryMessageContext) +ws.onUpgrade(WsUpgradeLogger) ``` The different flavors of `WsContext` expose different things, for example, @@ -453,30 +559,30 @@ The differences between the different contexts is small, and a full overview can You can learn about how Javalin handles WebSocket concurrency in [FAQ - Concurrency](#concurrency). ### WsBefore -The `app.wsBefore` adds a handler that runs before a WebSocket handler. +The `config.routes.wsBefore` adds a handler that runs before a WebSocket handler. You can have as many before-handlers as you want per WebSocket endpoint, and all events are supported. {% capture java %} -app.wsBefore(ws -> { +config.routes.wsBefore(ws -> { // runs before all WebSocket requests }); -app.wsBefore("/path/*", ws -> { +config.routes.wsBefore("/path/*", ws -> { // runs before websocket requests to /path/* }); {% endcapture %} {% capture kotlin %} -app.wsBefore { ws -> +config.routes.wsBefore { ws -> // runs before all WebSocket requests } -app.wsBefore("/path/*") { ws -> +config.routes.wsBefore("/path/*") { ws -> // runs before websocket requests to /path/* } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} ### WsEndpoint -A WebSocket endpoint is declared with `app.ws(path, handler)`. WebSocket handlers require unique paths. +A WebSocket endpoint is declared with `config.routes.ws(path, handler)`. WebSocket handlers require unique paths. {% capture java %} -app.ws("/websocket/{path}", ws -> { +config.routes.ws("/websocket/{path}", ws -> { ws.onConnect(ctx -> System.out.println("Connected")); ws.onMessage(ctx -> { User user = ctx.messageAsClass(User.class); // convert from json @@ -488,7 +594,7 @@ app.ws("/websocket/{path}", ws -> { }); {% endcapture %} {% capture kotlin %} -app.ws("/websocket/{path}") { ws -> +config.routes.ws("/websocket/{path}") { ws -> ws.onConnect { ctx -> println("Connected") } ws.onMessage { ctx -> val user = ctx.messageAsClass(); // convert from json @@ -502,27 +608,78 @@ app.ws("/websocket/{path}") { ws -> {% include macros/docsSnippet.html java=java kotlin=kotlin %} ### WsAfter -The `app.wsAfter` adds a handler that runs after a WebSocket handler. +The `config.routes.wsAfter` adds a handler that runs after a WebSocket handler. You can have as many after-handlers as you want per WebSocket endpoint, and all events are supported. {% capture java %} -app.wsAfter(ws -> { +config.routes.wsAfter(ws -> { // runs after all WebSocket requests }); -app.wsAfter("/path/*", ws -> { +config.routes.wsAfter("/path/*", ws -> { // runs after websocket requests to /path/* }); {% endcapture %} {% capture kotlin %} -app.wsAfter { ws -> +config.routes.wsAfter { ws -> // runs after all WebSocket requests } -app.wsAfter("/path/*") { ws -> +config.routes.wsAfter("/path/*") { ws -> // runs after websocket requests to /path/* } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} +### WsBeforeUpgrade +The `config.routes.wsBeforeUpgrade` adds a handler that runs before the WebSocket upgrade is attempted. +This is an HTTP handler (not a WsConfig handler), so you have access to the regular `Context`. +It's useful for authentication, validation, or rejecting upgrade requests before they happen. + +{% capture java %} +config.routes.wsBeforeUpgrade(ctx -> { + // runs before all WebSocket upgrade requests +}); +config.routes.wsBeforeUpgrade("/path/*", ctx -> { + // runs before websocket upgrade requests to /path/* + if (!isAuthenticated(ctx)) { + throw new UnauthorizedResponse(); + } +}); +{% endcapture %} +{% capture kotlin %} +config.routes.wsBeforeUpgrade { ctx -> + // runs before all WebSocket upgrade requests +} +config.routes.wsBeforeUpgrade("/path/*") { ctx -> + // runs before websocket upgrade requests to /path/* + if (!isAuthenticated(ctx)) { + throw UnauthorizedResponse() + } +} +{% endcapture %} +{% include macros/docsSnippet.html java=java kotlin=kotlin %} + +### WsAfterUpgrade +The `config.routes.wsAfterUpgrade` adds a handler that runs after the WebSocket upgrade is attempted. +Like `wsBeforeUpgrade`, this is an HTTP handler with access to the regular `Context`. + +{% capture java %} +config.routes.wsAfterUpgrade(ctx -> { + // runs after all WebSocket upgrade requests +}); +config.routes.wsAfterUpgrade("/path/*", ctx -> { + // runs after websocket upgrade requests to /path/* +}); +{% endcapture %} +{% capture kotlin %} +config.routes.wsAfterUpgrade { ctx -> + // runs after all WebSocket upgrade requests +} +config.routes.wsAfterUpgrade("/path/*") { ctx -> + // runs after websocket upgrade requests to /path/* +} +{% endcapture %} +{% include macros/docsSnippet.html java=java kotlin=kotlin %} + ### WsContext The `WsContext` object provides you with everything you need to handle a websocket-request. It contains the underlying websocket session and servlet-request, and convenience methods for sending @@ -536,7 +693,7 @@ send(byteBuffer) // send bytes to client sendAsClass(obj, clazz) // serialize object to json string and send it to client // Upgrade Context methods (getters) -matchedPath() // get the path that was used to match this request (ex, "/hello/{name}") +endpoint().path // get the path that was used to match this request (ex, "/hello/{name}") host() // host as string queryParam("name") // query param by name as string @@ -611,7 +768,7 @@ you can use this safely in multiple locations and from multiple threads. You can import all the HTTP methods with `import static io.javalin.apibuilder.ApiBuilder.*`. {% capture java %} -config.router.apiBuilder(() -> { +config.routes.apiBuilder(() -> { path("/users", () -> { get(UserController::getAllUsers); post(UserController::createUser); @@ -625,7 +782,7 @@ config.router.apiBuilder(() -> { }); {% endcapture %} {% capture kotlin %} -config.router.apiBuilder { +config.routes.apiBuilder { path("/users") { get(UserController::getAllUsers) post(UserController::createUser) @@ -647,12 +804,12 @@ This means that `path("api", ...)` and `path("/api", ...)` are equivalent. The `CrudHandler` is an interface that can be used within an `apiBuilder()` call: {% capture java %} -config.router.apiBuilder(() -> { +config.routes.apiBuilder(() -> { crud("users/{user-id}", new UserController()); }); {% endcapture %} {% capture kotlin %} -config.router.apiBuilder { +config.routes.apiBuilder { crud("users/{user-id}", UserController()) } {% endcapture %} @@ -787,7 +944,7 @@ val manyErrors = listOf(ageValidator, otherValidator, ...).collectErrors() When a `Validator` throws, it is mapped by: ```kotlin -app.exception(ValidationException::class.java) { e, ctx -> +config.routes.exception(ValidationException::class.java) { e, ctx -> ctx.json(e.errors).status(400) } ``` @@ -795,12 +952,12 @@ app.exception(ValidationException::class.java) { e, ctx -> You can override this by doing: {% capture java %} -app.exception(ValidationException.class, (e, ctx) -> { +config.routes.exception(ValidationException.class, (e, ctx) -> { // your code }); {% endcapture %} {% capture kotlin %} -app.exception(ValidationException::class.java) { e, ctx -> +config.routes.exception(ValidationException::class.java) { e, ctx -> // your code } {% endcapture %} @@ -827,10 +984,10 @@ set per-endpoint authentication and/or authorization. In Javalin 6, this has bee replaced with the `beforeMatched` handler. You can read more about this in the [Javalin 5 to 6 migration guide](/migration-guide-javalin-5-to-6#the-accessmanager-interface-has-been-removed). -To manage access in Javalin 6, you would do something like this: +To manage access in Javalin 7, you would do something like this: {% capture java %} -app.beforeMatched(ctx -> { +config.routes.beforeMatched(ctx -> { var userRole = getUserRole(ctx); // some user defined function that returns a user role if (!ctx.routeRoles().contains(userRole)) { // routeRoles are provided through the Context interface throw new UnauthorizedResponse(); // request will have to be explicitly stopped by throwing an exception @@ -838,7 +995,7 @@ app.beforeMatched(ctx -> { }); {% endcapture %} {% capture kotlin %} -app.beforeMatched { ctx -> +config.routes.beforeMatched { ctx -> val userRole = getUserRole(ctx) // some user defined function that returns a user role if (!ctx.routeRoles().contains(userRole)) { // routeRoles are provided through the Context interface throw UnauthorizedResponse() // request will have to be explicitly stopped by throwing an exception @@ -850,12 +1007,12 @@ app.beforeMatched { ctx -> The roles are set when you declare your endpoints: {% capture java %} -app.get("/public", ctx -> ctx.result("Hello public"), Role.OPEN); -app.get("/private", ctx -> ctx.result("Hello private"), Role.LOGGED_IN); +config.routes.get("/public", ctx -> ctx.result("Hello public"), Role.OPEN); +config.routes.get("/private", ctx -> ctx.result("Hello private"), Role.LOGGED_IN); {% endcapture %} {% capture kotlin %} -app.get("/public", { ctx -> ctx.result("Hello public") }, Role.OPEN) -app.get("/private", { ctx -> ctx.result("Hello private") }, Role.LOGGED_IN) +config.routes.get("/public", { ctx -> ctx.result("Hello public") }, Role.OPEN) +config.routes.get("/private", { ctx -> ctx.result("Hello private") }, Role.LOGGED_IN) {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -864,7 +1021,7 @@ Javalin comes with a built in class called `HttpResponseException`, which can be If the client accepts JSON, a JSON object is returned. Otherwise a plain text response is returned. ```java -app.post("/") { throw new ForbiddenResponse("Off limits!") } +config.routes.post("/", ctx -> { throw new ForbiddenResponse("Off limits!"); }); ``` If client accepts JSON: ```java @@ -906,6 +1063,9 @@ Returns a [409 Conflict](https://developer.mozilla.org/en-US/docs/Web/HTTP/Statu ### GoneResponse Returns a [410 Gone](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/410) response with the default title `Gone`. +### TooManyRequestsResponse +Returns a [429 Too Many Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) response with the default title `Too many requests`. + ### InternalServerErrorResponse Returns a [500 Internal Server Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) response with the default title `Internal server error`. @@ -921,47 +1081,51 @@ Returns a [504 Gateway Timeout](https://developer.mozilla.org/en-US/docs/Web/HTT ## Exception Mapping All handlers (before, endpoint, after, ws) can throw `Exception` (and any subclass of `Exception`). -The `app.exception()` and `app.wsException()` methods gives you a way of handling these exceptions: +The `config.routes.exception()` and `config.routes.wsException()` methods give you a way of handling these exceptions: {% capture java %} -// HTTP exceptions -app.exception(NullPointerException.class, (e, ctx) -> { - // handle nullpointers here -}); +Javalin.create(config -> { + // HTTP exceptions + config.routes.exception(NullPointerException.class, (e, ctx) -> { + // handle nullpointers here + }); -app.exception(Exception.class, (e, ctx) -> { - // handle general exceptions here - // will not trigger if more specific exception-mapper found -}); + config.routes.exception(Exception.class, (e, ctx) -> { + // handle general exceptions here + // will not trigger if more specific exception-mapper found + }); -// WebSocket exceptions -app.wsException(NullPointerException.class, (e, ctx) -> { - // handle nullpointers here -}); + // WebSocket exceptions + config.routes.wsException(NullPointerException.class, (e, ctx) -> { + // handle nullpointers here + }); -app.wsException(Exception.class, (e, ctx) -> { - // handle general exceptions here - // will not trigger if more specific exception-mapper found + config.routes.wsException(Exception.class, (e, ctx) -> { + // handle general exceptions here + // will not trigger if more specific exception-mapper found + }); }); {% endcapture %} {% capture kotlin %} -// HTTP exceptions -app.exception(NullPointerException::class.java) { e, ctx -> - // handle nullpointers here -} +Javalin.create { config -> + // HTTP exceptions + config.routes.exception(NullPointerException::class.java) { e, ctx -> + // handle nullpointers here + } -app.exception(Exception::class.java) { e, ctx -> - // handle general exceptions here - // will not trigger if more specific exception-mapper found -} + config.routes.exception(Exception::class.java) { e, ctx -> + // handle general exceptions here + // will not trigger if more specific exception-mapper found + } -// WebSocket exceptions -app.wsException(NullPointerException::class.java) { e, ctx -> - // handle nullpointers here -} + // WebSocket exceptions + config.routes.wsException(NullPointerException::class.java) { e, ctx -> + // handle nullpointers here + } -app.wsException(Exception::class.java) { e, ctx -> - // handle general exceptions here - // will not trigger if more specific exception-mapper found + config.routes.wsException(Exception::class.java) { e, ctx -> + // handle general exceptions here + // will not trigger if more specific exception-mapper found + } } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -969,13 +1133,17 @@ app.wsException(Exception::class.java) { e, ctx -> ## Error Mapping HTTP Error mapping is similar to exception mapping, but it operates on HTTP status codes instead of Exceptions: {% capture java %} -app.error(404, ctx -> { - ctx.result("Generic 404 message"); +Javalin.create(config -> { + config.routes.error(404, ctx -> { + ctx.result("Generic 404 message"); + }); }); {% endcapture %} {% capture kotlin %} -app.error(404) { ctx -> - ctx.result("Generic 404 message") +Javalin.create { config -> + config.routes.error(404) { ctx -> + ctx.result("Generic 404 message") + } } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -983,17 +1151,23 @@ app.error(404) { ctx -> It can make sense to use them together: {% capture java %} -app.exception(FileNotFoundException.class, (e, ctx) -> { - ctx.status(404); -}).error(404, ctx -> { - ctx.result("Generic 404 message"); +Javalin.create(config -> { + config.routes.exception(FileNotFoundException.class, (e, ctx) -> { + ctx.status(404); + }); + config.routes.error(404, ctx -> { + ctx.result("Generic 404 message"); + }); }); {% endcapture %} {% capture kotlin %} -app.exception(FileNotFoundException::class.java) { e, ctx -> - ctx.status(404) -}.error(404) { ctx -> - ctx.result("Generic 404 message") +Javalin.create { config -> + config.routes.exception(FileNotFoundException::class.java) { e, ctx -> + ctx.status(404) + } + config.routes.error(404) { ctx -> + ctx.result("Generic 404 message") + } } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -1001,13 +1175,17 @@ app.exception(FileNotFoundException::class.java) { e, ctx -> You can also include the content type when declaring your error mappers: {% capture java %} -app.error(404, "html", ctx -> { - ctx.html("Generic 404 message"); +Javalin.create(config -> { + config.routes.error(404, "html", ctx -> { + ctx.html("Generic 404 message"); + }); }); {% endcapture %} {% capture kotlin %} -app.error(404, "html") { ctx -> - ctx.html("Generic 404 message") +Javalin.create { config -> + config.routes.error(404, "html") { ctx -> + ctx.html("Generic 404 message") + } } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -1016,17 +1194,17 @@ This can be useful if you, for example, want one set of error handlers for HTML, ## Server-sent Events Server-sent events (often also called event source) are very simple in Javalin. -You call `app.sse()`, which gives you access to the connected `SseClient`: +You call `config.routes.sse()`, which gives you access to the connected `SseClient`: {% capture java %} -app.sse("/sse", client -> { +config.routes.sse("/sse", client -> { client.sendEvent("connected", "Hello, SSE"); client.onClose(() -> System.out.println("Client disconnected")); client.close(); // close the client }); {% endcapture %} {% capture kotlin %} -app.sse("/sse") { client -> +config.routes.sse("/sse") { client -> client.sendEvent("connected", "Hello, SSE") client.onClose { println("Client disconnected") } client.close() // close the client @@ -1039,16 +1217,16 @@ Clients are automatically closed when leaving the handler, if you need to use th {% capture java %} Queue clients = new ConcurrentLinkedQueue(); -app.sse("/sse", client -> { +config.routes.sse("/sse", client -> { client.keepAlive(); - client.onClose(() - > clients.remove(client)); + client.onClose(() -> clients.remove(client)); clients.add(client); }); {% endcapture %} {% capture kotlin %} val clients = ConcurrentLinkedQueue() -app.sse("/sse") { client -> +config.routes.sse("/sse") { client -> client.keepAlive() client.onClose { clients.remove(client) } clients.add(client) @@ -1067,7 +1245,7 @@ onClose(runnable) // callback which runs when a client keepAlive() // keeps the connection alive. useful if you want to keep a list of clients to broadcast to. close() // closes the connection terminated() // returns true if the connection has been closed -ctx // the Context from when the client connected (to fetch query-params, etc) +ctx() // the Context from when the client connected (to fetch query-params, etc) ``` ## Configuration @@ -1079,25 +1257,27 @@ but there are also a few direct properties and functions: Javalin.create(config -> { config.http // The http layer configuration: etags, request size, timeout, etc config.router // The routing configuration: context path, slash treatment, etc + config.routes // Routes configuration: HTTP handlers, WebSocket handlers, SSE, exceptions, errors config.jetty // The embedded Jetty webserver configuration config.staticFiles // Static files and webjars configuration - config.spaRoot = // Single Page Application roots configuration + config.spaRoot // Single Page Application roots configuration config.requestLogger // Request Logger configuration: http and websocket loggers config.bundledPlugins // Bundled plugins configuration: enable bundled plugins or add custom ones - config.events // Events configuration - config.vue // Vue Plugin configuration + config.events // Events configuration: server lifecycle events, handler added events config.contextResolver // Context resolver implementation configuration config.validation // Default validator configuration - config.useVirtualThreads // Use virtual threads (based on Java Project Loom) - config.showJavalinBanner // Show the Javalin banner in the logs - config.startupWatcherEnabled // Print warning if instance was not started after 5 seconds - config.pvt // This is "private", only use it if you know what you're doing + config.concurrency.useVirtualThreads // Use virtual threads (based on Java Project Loom) + config.startup.showJavalinBanner // Show the Javalin banner in the logs + config.startup.showOldJavalinVersionWarning // Show a warning if an old Javalin version is being used + config.startup.startupWatcherEnabled // Print warning if instance was not started after 5 seconds - config.events(listenerConfig) // Add an event listener config.jsonMapper(jsonMapper) // Set a custom JsonMapper config.fileRenderer(fileRenderer) // Set a custom FileRenderer + config.resourceHandler(resourceHandler) // Set a custom ResourceHandler for static files (Jetty-free) config.registerPlugin(plugin) // Register a plugin config.appData(key, data) // Store data on the Javalin instance + + config.unsafe // Advanced/unsafe API providing access to internal Javalin configuration (use with caution) }); ``` @@ -1108,34 +1288,34 @@ All available subconfigs are explained in the sections below. Javalin.create(config -> { config.http.generateEtags = booleanValue; // if javalin should generate etags for dynamic responses (not static files) config.http.prefer405over404 = booleanValue; // return 405 instead of 404 if path is mapped to different HTTP method - config.http.maxRequestSize = longValue; // the max size of request body that can be accessed without using using an InputStream - config.http.responseBufferSize = longValue; // the size of the response buffer (default 32kb) + config.http.maxRequestSize = longValue; // the max size of request body that can be accessed without using an InputStream + config.http.responseBufferSize = intValue; // the size of the response buffer (default 32kb) config.http.defaultContentType = stringValue; // the default content type config.http.asyncTimeout = longValue; // timeout in milliseconds for async requests (0 means no timeout) config.http.strictContentTypes = booleanValue; // throw exception if e.g content-type is missing/incorrect when attempting to parse JSON - config.http.customCompression(strategy); // set a custom compression strategy - config.http.brotliAndGzipCompression(lvl, lvl); // enable brotli and gzip compression with the specified levels - config.http.gzipOnlyCompression(lvl); // enable gzip compression with the specified level - config.http.brotliOnlyCompression(lvl); // enable brotli compression with the specified level - config.http.disableCompression(); // disable compression + config.http.compressionStrategy = new CompressionStrategy(new Brotli(lvl), new Gzip(lvl)); + config.http.compressionStrategy = new CompressionStrategy(null, new Gzip(lvl)); + config.http.compressionStrategy = new CompressionStrategy(new Brotli(lvl), null); + config.http.compressionStrategy = new CompressionStrategy(null, null, new Zstd(lvl)); + config.http.compressionStrategy = CompressionStrategy.NONE; }); {% endcapture %} {% capture kotlin %} Javalin.create { config -> config.http.generateEtags = booleanValue // if javalin should generate etags for dynamic responses (not static files) config.http.prefer405over404 = booleanValue // return 405 instead of 404 if path is mapped to different HTTP method - config.http.maxRequestSize = longValue // the max size of request body that can be accessed without using using an InputStream - config.http.responseBufferSize = longValue // the size of the response buffer (default 32kb) + config.http.maxRequestSize = longValue // the max size of request body that can be accessed without using an InputStream + config.http.responseBufferSize = intValue // the size of the response buffer (default 32kb) config.http.defaultContentType = stringValue // the default content type config.http.asyncTimeout = longValue // timeout in milliseconds for async requests (0 means no timeout) config.http.strictContentTypes = booleanValue // throw exception if e.g content-type is missing/incorrect when attempting to parse JSON - config.http.customCompression(strategy) // set a custom compression strategy - config.http.brotliAndGzipCompression(lvl, lvl) // enable brotli and gzip compression with the specified levels - config.http.gzipOnlyCompression(lvl) // enable gzip compression with the specified level - config.http.brotliOnlyCompression(lvl) // enable brotli compression with the specified level - config.http.disableCompression() // disable compression + config.http.compressionStrategy = CompressionStrategy(Brotli(lvl), Gzip(lvl)) + config.http.compressionStrategy = CompressionStrategy(null, Gzip(lvl)) + config.http.compressionStrategy = CompressionStrategy(Brotli(lvl), null) + config.http.compressionStrategy = CompressionStrategy(null, null, Zstd(lvl)) + config.http.compressionStrategy = CompressionStrategy.NONE } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -1166,13 +1346,13 @@ Javalin.create { config -> ### JettyConfig {% capture java %} Javalin.create(config -> { - config.jetty.defaultHost = "localhost"; // set the default host for Jetty - config.jetty.defaultPort = 1234; // set the default port for Jetty + config.jetty.host = "localhost"; // set the host for Jetty + config.jetty.port = 1234; // set the port for Jetty config.jetty.threadPool = new ThreadPool(); // set the thread pool for Jetty - config.jetty.timeoutStatus = 408; // set the timeout status for Jetty (default 500) - config.jetty.clientAbortStatus = 499; // set the abort status for Jetty (default 500) + config.jetty.timeoutStatus = 408; // set the timeout status for Jetty (default 408) + config.jetty.clientAbortStatus = 499; // set the abort status for Jetty (default 499) config.jetty.multipartConfig = new MultipartConfig(); // set the multipart config for Jetty - config.jetty.modifyJettyWebSocketServletFactory(factory -> {}); // modify the JettyWebSocketServletFactory + config.jetty.modifyWebSocketServletFactory(factory -> {}); // modify the JettyWebSocketServletFactory config.jetty.modifyServer(server -> {}); // modify the Jetty Server config.jetty.modifyServletContextHandler(handler -> {}); // modify the ServletContextHandler (you can set a SessionHandler here) config.jetty.modifyHttpConfiguration(httpConfig -> {}); // modify the HttpConfiguration @@ -1181,13 +1361,13 @@ Javalin.create(config -> { {% endcapture %} {% capture kotlin %} Javalin.create { config -> - config.jetty.defaultHost = "localhost" // set the default host for Jetty - config.jetty.defaultPort = 1234 // set the default port for Jetty + config.jetty.host = "localhost" // set the host for Jetty + config.jetty.port = 1234 // set the port for Jetty config.jetty.threadPool = ThreadPool() // set the thread pool for Jetty - config.jetty.timeoutStatus = 408 // set the timeout status for Jetty (default 500) - config.jetty.clientAbortStatus = 499 // set the abort status for Jetty (default 500) + config.jetty.timeoutStatus = 408 // set the timeout status for Jetty (default 408) + config.jetty.clientAbortStatus = 499 // set the abort status for Jetty (default 499) config.jetty.multipartConfig = MultipartConfig() // set the multipart config for Jetty - config.jetty.modifyJettyWebSocketServletFactory { factory -> } // modify the JettyWebSocketServletFactory + config.jetty.modifyWebSocketServletFactory { factory -> } // modify the JettyWebSocketServletFactory config.jetty.modifyServer { server -> } // modify the Jetty Server config.jetty.modifyServletContextHandler { handler -> } // modify the ServletContextHandler (you can set a SessionHandler here) config.jetty.modifyHttpConfiguration { httpConfig -> } // modify the HttpConfiguration @@ -1242,7 +1422,7 @@ Javalin.create { config -> {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} -You can add a WebSocket logger by calling `config.requestLogger.ws()`. The method takes a the same arguments as a normal `app.ws()` call, +You can add a WebSocket logger by calling `config.requestLogger.ws()`. The method takes a the same arguments as a normal `config.routes.ws()` call, and can be used to log events of all types. The following example just shows `onMessage`, but `onConnect`, `onError` and `onClose` are all available: @@ -1257,9 +1437,9 @@ Javalin.create(config -> { {% endcapture %} {% capture kotlin %} Javalin.create { config -> - config.requestLogger.ws(ws -> { + config.requestLogger.ws { ws -> ws.onMessage { ctx -> - println("Received: " + ctx.message()); + println("Received: " + ctx.message()) } } } @@ -1328,11 +1508,12 @@ Javalin.create(config -> { staticFiles.hostedPath = "/"; // change to host files on a subpath, like '/assets' staticFiles.directory = "/public"; // the directory where your files are located staticFiles.location = Location.CLASSPATH; // Location.CLASSPATH (jar) or Location.EXTERNAL (file system) - staticFiles.precompress = false; // if the files should be pre-compressed and cached in memory (optimization) + staticFiles.precompressMaxSize = 0; // max size for pre-compression in bytes (-1 to disable, 0 for all sizes) staticFiles.aliasCheck = null; // you can configure this to enable symlinks (= ContextHandler.ApproveAliases()) staticFiles.headers = Map.of(...); // headers that will be set for the files staticFiles.skipFileFunction = req -> false; // you can use this to skip certain files in the dir, based on the HttpServletRequest staticFiles.mimeTypes.add(mimeType, ext); // you can add custom mimetypes for extensions + staticFiles.roles = Set.of(roles); // roles that are allowed to access the static files (used in beforeMatched) }); }); {% endcapture %} @@ -1342,11 +1523,12 @@ Javalin.create { config -> staticFiles.hostedPath = "/" // change to host files on a subpath, like '/assets' staticFiles.directory = "/public" // the directory where your files are located staticFiles.location = Location.CLASSPATH // Location.CLASSPATH (jar) or Location.EXTERNAL (file system) - staticFiles.precompress = false // if the files should be pre-compressed and cached in memory (optimization) + staticFiles.precompressMaxSize = 0 // max size for pre-compression in bytes (-1 to disable, 0 for all sizes) staticFiles.aliasCheck = null // you can configure this to enable symlinks (= ContextHandler.ApproveAliases()) staticFiles.headers = mapOf(...) // headers that will be set for the files staticFiles.skipFileFunction = { req -> false } // you can use this to skip certain files in the dir, based on the HttpServletRequest staticFiles.mimeTypes.add(mimeType, ext) // you can add custom mimetypes for extensions + staticFiles.roles = setOf(roles) // roles that are allowed to access the static files (used in beforeMatched) } } {% endcapture %} @@ -1360,6 +1542,25 @@ they will be available at `/webjars/name/version/file.ext`. WebJars can be found on [https://www.webjars.org/](https://www.webjars.org/). Everything available through NPM is also available through WebJars. +#### Jetty-free static file serving +If you want to serve static files without depending on Jetty's resource handling, +you can use the `JavalinStaticResourceHandler`: + +{% capture java %} +Javalin.create(config -> { + config.resourceHandler(new JavalinStaticResourceHandler()); +}); +{% endcapture %} +{% capture kotlin %} +Javalin.create { config -> + config.resourceHandler(JavalinStaticResourceHandler()) +} +{% endcapture %} +{% include macros/docsSnippet.html java=java kotlin=kotlin %} + +This is useful if you want to use Javalin with a different embedded server, +or if you want to avoid Jetty-specific dependencies for static file handling. + If you are building a Single Page Application (SPA), you should have a look at the [SpaRootConfig](#sparootconfig) @@ -1382,8 +1583,11 @@ dependency to your project: ### Server setup -Javalin runs on an embedded [Jetty](http://eclipse.org/jetty/). To start and stop the server, -use `start()` and `stop`: +Javalin runs on an embedded [Jetty](http://eclipse.org/jetty/). + +**Note:** Javalin 7 uses **Jetty 12** (previously Jetty 11), which means servlet packages have changed from `javax.servlet.*` to `jakarta.servlet.*`. + +To start and stop the server, use `start()` and `stop`: ```java Javalin app = Javalin.create() @@ -1391,32 +1595,51 @@ Javalin app = Javalin.create() .stop() // stop server (sync/blocking) ``` +There is also a convenience method `Javalin.start(config)` that creates and starts a Javalin instance in one call: + +{% capture java %} +Javalin app = Javalin.start(config -> { + config.jetty.port = 8080; + config.routes.get("/", ctx -> ctx.result("Hello World")); +}); +{% endcapture %} +{% capture kotlin %} +val app = Javalin.start { config -> + config.jetty.port = 8080 + config.routes.get("/") { ctx -> ctx.result("Hello World") } +} +{% endcapture %} +{% include macros/docsSnippet.html java=java kotlin=kotlin %} + The `app.start()` method spawns a user thread, starts the server, and then returns. Your program will not exit until this thread is terminated by calling `app.stop()`. If you want to do a clean shutdown when the program is exiting, you could use: ```java +Javalin app = Javalin.create(config -> { + config.events.serverStopping(() -> { /* Your code here */ }); + config.events.serverStopped(() -> { /* Your code here */ }); +}); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { app.stop(); })); - -app.events(event -> { - event.serverStopping(() -> { /* Your code here */ }); - event.serverStopped(() -> { /* Your code here */ }); -}); ``` -If you want graceful shutdown, you can configure the server using the `modifyServer` method: +Javalin's default Jetty server includes a `StatisticsHandler`, so graceful shutdown works out of the box. +When `app.stop()` is called, the server will wait for active requests to complete before shutting down. + +If you are using a custom server, make sure to add a `StatisticsHandler` for graceful shutdown: {% capture java %} Javalin.create(config -> { - config.jetty.modifyServer(server -> server.setStopTimeout(5_000)); // wait 5 seconds for existing requests to finish + config.jetty.modifyServer(server -> server.insertHandler(new StatisticsHandler())); }); {% endcapture %} {% capture kotlin %} Javalin.create { config -> - config.jetty.modifyServer { server -> server.setStopTimeout(5_000) } // wait 5 seconds for existing requests to finish + config.jetty.modifyServer { server -> server.insertHandler(StatisticsHandler()) } } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -1438,25 +1661,20 @@ You can configure your embedded jetty-server with a handler-chain and Javalin will attach it's own handlers to the end of this chain. {% capture java %} StatisticsHandler statisticsHandler = new StatisticsHandler(); +Server server = new Server(); +server.setHandler(statisticsHandler); -Javalin.create(config -> { - config.server(() -> { - Server server = new Server(); - server.setHandler(statisticsHandler); - return server; - }) -}); +Javalin app = Javalin.create(config -> { /* your config */ }); +app.unsafe.jettyInternal.server = server; +app.start(); {% endcapture %} {% capture kotlin %} val statisticsHandler = StatisticsHandler() +val server = Server().apply { handler = statisticsHandler } -Javalin.create { config -> - config.server { - Server().apply { - handler = statisticsHandler - } - } -}.start(); +val app = Javalin.create { /* your config */ } +app.unsafe.jettyInternal.server = server +app.start() {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -1473,36 +1691,71 @@ An example of a custom server with SSL can be found in the examples, A custom HTTP2 server is a bit more work to set up, but we have a repo with a fully functioning example server in both Kotlin and Java: [javalin-http2-example](https://github.com/tipsy/javalin-http2-example) +### BundledPluginsConfig +Javalin comes with several bundled plugins that can be enabled through `config.bundledPlugins`: + +{% capture java %} +Javalin.create(config -> { + config.bundledPlugins.enableRouteOverview("/routes"); // HTML/JSON overview of all routes + config.bundledPlugins.enableRouteOverview("/routes", roles); // route overview with access roles + config.bundledPlugins.enableBasicAuth("username", "password"); // basic auth for all routes + config.bundledPlugins.enableGlobalHeaders(headers -> { ... }); // add global response headers + config.bundledPlugins.enableCors(cors -> { ... }); // enable CORS + config.bundledPlugins.enableHttpAllowedMethodsOnRoutes(); // auto Access-Control-Allow-Methods for OPTIONS + config.bundledPlugins.enableDevLogging(); // development request/response logger + config.bundledPlugins.enableDevLogging(devCfg -> { ... }); // dev logger with custom config + config.bundledPlugins.enableRedirectToLowercasePaths(); // redirect /Users/John to /users/John + config.bundledPlugins.enableSslRedirects(); // redirect HTTP to HTTPS +}); +{% endcapture %} +{% capture kotlin %} +Javalin.create { config -> + config.bundledPlugins.enableRouteOverview("/routes") // HTML/JSON overview of all routes + config.bundledPlugins.enableRouteOverview("/routes", roles) // route overview with access roles + config.bundledPlugins.enableBasicAuth("username", "password") // basic auth for all routes + config.bundledPlugins.enableGlobalHeaders { headers -> } // add global response headers + config.bundledPlugins.enableCors { cors -> } // enable CORS + config.bundledPlugins.enableHttpAllowedMethodsOnRoutes() // auto Access-Control-Allow-Methods for OPTIONS + config.bundledPlugins.enableDevLogging() // development request/response logger + config.bundledPlugins.enableDevLogging { devCfg -> } // dev logger with custom config + config.bundledPlugins.enableRedirectToLowercasePaths() // redirect /Users/John to /users/John + config.bundledPlugins.enableSslRedirects() // redirect HTTP to HTTPS +} +{% endcapture %} +{% include macros/docsSnippet.html java=java kotlin=kotlin %} + ## Lifecycle events Javalin has events for server start/stop, as well as for when handlers are added. The snippet below shows all of them in action: {% capture java %} -Javalin app = Javalin.create().events(event -> { - event.serverStarting(() -> { ... }); - event.serverStarted(() -> { ... }); - event.serverStartFailed(() -> { ... }); - event.serverStopping(() -> { ... }); - event.serverStopped(() -> { ... }); - event.handlerAdded(handlerMetaInfo -> { ... }); - event.wsHandlerAdded(wsHandlerMetaInfo -> { ... }); +Javalin app = Javalin.create(config -> { + config.events.serverStarting(() -> { ... }); + config.events.serverStarted(() -> { ... }); + config.events.serverStartFailed(() -> { ... }); + config.events.serverStopping(() -> { ... }); + config.events.serverStopped(() -> { ... }); + config.events.serverStopFailed(() -> { ... }); + config.events.handlerAdded(handlerMetaInfo -> { ... }); + config.events.wsHandlerAdded(wsHandlerMetaInfo -> { ... }); }); app.start() // serverStarting -> (serverStarted || serverStartFailed) -app.stop() // serverStopping -> serverStopped +app.stop() // serverStopping -> (serverStopped || serverStopFailed) {% endcapture %} {% capture kotlin %} -Javalin app = Javalin.create().events { event -> - event.serverStarting { ... } - event.serverStarted { ... } - event.serverStartFailed { ... } - event.serverStopping { ... } - event.serverStopped { ... } - event.handlerAdded { handlerMetaInfo -> } - event.wsHandlerAdded { wsHandlerMetaInfo -> } +val app = Javalin.create { config -> + config.events.serverStarting { ... } + config.events.serverStarted { ... } + config.events.serverStartFailed { ... } + config.events.serverStopping { ... } + config.events.serverStopped { ... } + config.events.serverStopFailed { ... } + config.events.handlerAdded { handlerMetaInfo -> } + config.events.wsHandlerAdded { wsHandlerMetaInfo -> } } app.start() // serverStarting -> (serverStarted || serverStartFailed) -app.stop() // serverStopping -> serverStopped +app.stop() // serverStopping -> (serverStopped || serverStopFailed) {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -1519,38 +1772,45 @@ Frequently asked questions. The Javalin request lifecycle is pretty straightforward. The following snippet covers every place you can hook into: ```java -Javalin#before // runs first, can throw exception (which will skip any endpoint handlers) -Javalin#get/post/patch/etc // runs second, can throw exception -Javalin#error // runs third, can throw exception -Javalin#after // runs fourth, can throw exception -Javalin#exception // runs any time a handler throws (cannot throw exception) -JavalinConfig#requestLogger // runs after response is written to client -JavalinConfig#accessManager // wraps all your endpoint handlers in a lambda of your choice +config.routes.before // runs first, can throw exception (which will skip any endpoint handlers) +config.routes.beforeMatched // runs after a matching endpoint is found, can throw exception +config.routes.get/post/patch/etc // runs third, can throw exception +config.routes.afterMatched // runs after the endpoint handler (only if a match was found) +config.routes.error // runs fifth, can throw exception +config.routes.after // runs sixth, can throw exception +config.routes.exception // runs any time a handler throws (cannot throw exception) +JavalinConfig#requestLogger // runs after response is written to client ``` --- ### Rate limiting -There is a very simple rate limiter included in Javalin. -You can call it in the beginning of your endpoint `Handler` functions: +There is a very simple rate limiter included in Javalin, available via `RateLimitPlugin`. +First, register the plugin, then call it in your endpoint `Handler` functions via `ctx.with(RateLimitPlugin.class)`: {% capture java %} -app.get("/", ctx -> { - NaiveRateLimit.requestPerTimeUnit(ctx, 5, TimeUnit.MINUTES); // throws if rate limit is exceeded - ctx.status("Hello, rate-limited World!"); +Javalin.create(config -> { + config.registerPlugin(new RateLimitPlugin(cfg -> { + // optional: customize the key function (default: ip + method + path) + cfg.keyFunction = ctx -> ctx.ip(); + })); + config.routes.get("/", ctx -> { + ctx.with(RateLimitPlugin.class).requestPerTimeUnit(5, TimeUnit.MINUTES); // throws if rate limit is exceeded + ctx.result("Hello, rate-limited World!"); + }); }); - -// you can overwrite the key-function: -RateLimitUtil.keyFunction = ctx -> // uses (ip+method+endpointPath) by default {% endcapture %} {% capture kotlin %} -app.get("/") { ctx -> - NaiveRateLimit.requestPerTimeUnit(ctx, 5, TimeUnit.MINUTES) // throws if rate limit is exceeded - ctx.status("Hello, rate-limited World!") +Javalin.create { config -> + config.registerPlugin(RateLimitPlugin { cfg -> + // optional: customize the key function (default: ip + method + path) + cfg.keyFunction = { ctx -> ctx.ip() } + }) + config.routes.get("/") { ctx -> + ctx.with(RateLimitPlugin::class).requestPerTimeUnit(5, TimeUnit.MINUTES) // throws if rate limit is exceeded + ctx.result("Hello, rate-limited World!") + } } - -// you can overwrite the key-function: -RateLimitUtil.keyFunction = { ctx -> } // uses (ip+method+endpointPath) by default {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -1565,8 +1825,8 @@ Different endpoints can have different rate limits. It works as follows: ### Android -Due to [Jetty 11 not working on Android](https://github.com/javalin/website/issues/211#issuecomment-1438319603), -Javalin 5+ is not compatible either, but Javalin 4 is.\\ +Due to [Jetty 11+ not working on Android](https://github.com/javalin/website/issues/211#issuecomment-1438319603), +Javalin 5+ (which uses Jetty 11 or 12) is not compatible, but Javalin 4 is.\\ You can find the docs for Javalin 4 [here](/archive/docs/v4.6.X.html).\\ You can check the status of Jetty 11+ on Android [here](https://github.com/eclipse/jetty.project/issues/8912#issuecomment-1439716937). @@ -1574,8 +1834,8 @@ You can check the status of Jetty 11+ on Android [here](https://github.com/eclip ### Concurrency If your JRE supports project Loom, -Javalin will use a `newVirtualThreadPerTaskExecutor` for serving requests if you set the -`enableVirtualThreads` config option. +Javalin will use a `newVirtualThreadPerTaskExecutor` for serving requests if you set the +`config.concurrency.useVirtualThreads = true` config option. Otherwise, a `QueuedThreadPool` with 250 threads will be used. Each incoming request is handled by a dedicated thread, so all Handler implementations should be thread-safe. @@ -1646,13 +1906,13 @@ on another web server (such as Tomcat), you can use Maven or Gradle to exclude J ### Uploads Uploaded files are easily accessible via `ctx.uploadedFiles()`: {% capture java %} -app.post("/upload", ctx -> { +config.routes.post("/upload", ctx -> { ctx.uploadedFiles("files").forEach(uploadedFile -> FileUtil.streamToFile(uploadedFile.content(), "upload/" + uploadedFile.filename())); }); {% endcapture %} {% capture kotlin %} -app.post("/upload") { ctx -> +config.routes.post("/upload") { ctx -> ctx.uploadedFiles("files").forEach { uploadedFile -> FileUtil.streamToFile(uploadedFile.content(), "upload/${uploadedFile.filename()}") } @@ -1696,14 +1956,14 @@ private fun getRandomCatFactFuture(): CompletableFuture> { .uri(URI.create("https://catfact.ninja/fact")) .build() return httpClient.sendAsync(request, ofString()) -) +} {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} Now we can use this method in our Javalin app to return cat facts to the client asynchronously: {% capture java %} -app.get("/cat-facts", ctx -> { +config.routes.get("/cat-facts", ctx -> { ctx.future(() -> { return getRandomCatFactFuture() .thenAccept(response -> ctx.html(response.body()).status(response.statusCode())) @@ -1715,7 +1975,7 @@ app.get("/cat-facts", ctx -> { }); {% endcapture %} {% capture kotlin %} -app.get("/cat-facts") { ctx -> +config.routes.get("/cat-facts") { ctx -> ctx.future { getRandomCatFactFuture() .thenAccept { response -> ctx.html(response.body()).status(response.statusCode()) } @@ -1741,34 +2001,30 @@ If you want to execute a blocking task outside of the server ThreadPool, you can The snippet below shows the available overloads for the method: ``` -async(runnableTask) // Javalin's default executor, no timeout or timeout callback -async(timeout, onTimeout, runnableTask) // Javalin's default executor, custom timeout handling -async(executor, timeout, onTimeout, runnableTask) // custom everything! +async(runnableTask) // Javalin's default executor, no timeout or timeout callback +async(asyncConfig, runnableTask) // custom executor, timeout, and timeout callback via AsyncTaskConfig ``` Javalin will immediately start an async context and run the task on a dedicated executor service. It will resume the normal request flow (after-handlers, request-logging) once the task is done. -The snippet belows shows a full example with a custom timeout, timeout handler, and a task: +The snippet below shows a full example with a custom timeout, timeout handler, and a task: {% capture java %} -app.get("/async", ctx -> { - ctx.async( - 1000, // timeout in ms - () -> ctx.result("Request took too long"), // timeout callback - () -> ctx.result(someSlowResult) // some long running task - ); +config.routes.get("/async", ctx -> { + ctx.async(cfg -> { + cfg.timeout = 1000; // timeout in ms + cfg.onTimeout(c -> c.result("Request took too long")); // timeout callback + }, () -> ctx.result(someSlowResult)); // some long running task }); {% endcapture %} {% capture kotlin %} - -app.get("/async") { ctx -> - ctx.async( - 1000, // timeout in ms - { ctx.result("Request took too long") }, // timeout callback - { ctx.result(someSlowResult) // some long running task - ) +config.routes.get("/async") { ctx -> + ctx.async({ cfg -> + cfg.timeout = 1000 // timeout in ms + cfg.onTimeout { c -> c.result("Request took too long") } // timeout callback + }) { ctx.result(someSlowResult) } // some long running task } {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} @@ -1780,7 +2036,7 @@ app.get("/async") { ctx -> To configure the JsonMapper, you need to pass an object which implements the `JsonMapper` interface to `config.jsonMapper()`. -The `JsonMapper` interface has four optional methods: +The `JsonMapper` interface has five optional methods: ```java String toJsonString(Object obj, Type type) { // basic method for mapping to json @@ -1806,7 +2062,7 @@ If you need further config, you can update the default settings like this: {% capture java %} config.jsonMapper(new JavalinJackson().updateMapper(mapper -> { mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); -}); +})); {% endcapture %} {% capture kotlin %} config.jsonMapper(JavalinJackson().updateMapper { mapper -> @@ -1852,14 +2108,14 @@ val app = Javalin.create { it.jsonMapper(gsonMapper) }.start(7070) ### Adding other Servlets and Filters to Javalin Javalin is designed to work with other `Servlet` and `Filter` instances running on the Jetty Server. -Filters are pretty straighforward to add, since they don't finish the request. -If you need to add a serlvet there's an example in the repo: +Filters are pretty straightforward to add, since they don't finish the request. +If you need to add a servlet there's an example in the repo: [/src/test/java/io/javalin/examples/HelloWorldServlet.java#L21-L29](https://github.com/javalin/javalin/blob/master/javalin/src/test/java/io/javalin/examples/HelloWorldServlet.java#L21-L29) You can also use it to build simple proxy using `AsyncProxyServlet` that is part of Jetty: ```java -// Add org.eclipse.jetty:jetty-proxy to maven/gradle dependencies (e.g Javalin 5.3.2 uses Jetty 11.0.13) +// Add org.eclipse.jetty:jetty-proxy to maven/gradle dependencies (Javalin 7 uses Jetty 12) Javalin.create(config -> { config.jetty.modifyServletContextHandler(handler -> { ServletHolder proxyServlet = new ServletHolder(AsyncProxyServlet.Transparent.class); @@ -1896,14 +2152,32 @@ The default `FileRenderer` of Javalin is a singleton named `JavalinRenderer`, se the section below for more information. ### Default implementations -Javalin offers an artifact with several template engines, called `javalin-rendering`, -which follows the same version as the `javalin` artifact. You can learn more -about this at [/plugins/rendering](/plugins/rendering). +Javalin offers separate per-engine artifacts for template rendering (e.g. `javalin-rendering-freemarker`, +`javalin-rendering-velocity`, `javalin-rendering-thymeleaf`, etc.), which follow the same version as the +`javalin` artifact. You can learn more about this at [/plugins/rendering](/plugins/rendering). --- ### Vue support (JavalinVue) If you don't want to deal with NPM and frontend builds, Javalin has support for simplified Vue.js development. + +**Note:** In Javalin 7, JavalinVue is now a plugin. You need to register it using `config.registerPlugin(new JavalinVuePlugin(...))`. + +**Note:** The `LoadableData` JavaScript class is no longer included by default. +If you want to use `LoadableData`, you need to explicitly enable it: + +{% capture java %} +config.registerPlugin(new JavalinVuePlugin(vue -> { + vue.enableLoadableData = true; +})); +{% endcapture %} +{% capture kotlin %} +config.registerPlugin(JavalinVuePlugin { vue -> + vue.enableLoadableData = true +}) +{% endcapture %} +{% include macros/docsSnippet.html java=java kotlin=kotlin %} + This requires you to make a layout template, `src/main/resources/vue/layout.html`: ```markup @@ -1941,8 +2215,8 @@ which you can access like this: To map a path to a Vue component you use the `VueComponent` class: ```java -get("/messages", VueComponent("inbox-view")) -get("/messages/{user}", VueComponent("thread-view")) +config.routes.get("/messages", new VueComponent("inbox-view")); +config.routes.get("/messages/{user}", new VueComponent("thread-view")); ``` This will give you a lot of the benefits of a modern frontend architecture, @@ -1963,19 +2237,19 @@ which is 30 seconds by default in Jetty/Javalin. This is not a bug. ### Java lang Error handling Javalin has a default error handler for `java.lang.Error` that will log the error and return a 500. -The default error handler can be overridden using the private config: +The default error handler can be overridden using `config.router`: {% capture java %} -Javalin.create( cfg -> { - cfg.pvt.javaLangErrorHandler((res, error) -> { +Javalin.create(config -> { + config.router.javaLangErrorHandler((res, error) -> { res.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.getCode()); JavalinLogger.error("Exception occurred while servicing http-request", error); }); }); {% endcapture %} {% capture kotlin %} -Javalin.create { cfg -> - cfg.pvt.javaLangErrorHandler { res, error -> +Javalin.create { config -> + config.router.javaLangErrorHandler { res, error -> res.status = HttpStatus.INTERNAL_SERVER_ERROR.code JavalinLogger.error("Exception occurred while servicing http-request", error) } @@ -2028,6 +2302,7 @@ After switching the class loader, you may still receive a missing dependency err --- ### Documentation for previous versions +Docs for 6.7.0 (last 6.X version) can be found [here](/archive/docs/v6.X.html).\\ Docs for 5.6.X (last 5.X version) can be found [here](/archive/docs/v5.6.X.html).\\ Docs for 4.6.X (last 4.X version) can be found [here](/archive/docs/v4.6.X.html).\\ Docs for 3.13.X (last 3.X version) can be found [here](/archive/docs/v3.13.X.html).\\ diff --git a/pages/docs/migration-guide-1-2.md b/pages/docs/migration-guide-1-2.md index 0fb17b5..3639681 100644 --- a/pages/docs/migration-guide-1-2.md +++ b/pages/docs/migration-guide-1-2.md @@ -1,5 +1,7 @@ --- layout: default +noindex: true +sitemap: false title: Migration guide, v1 to v2 rightmenu: false permalink: /migration-guide-javalin-1-to-2 diff --git a/pages/docs/migration-guide-2-3.md b/pages/docs/migration-guide-2-3.md index 718bd3f..8e10411 100644 --- a/pages/docs/migration-guide-2-3.md +++ b/pages/docs/migration-guide-2-3.md @@ -1,5 +1,7 @@ --- layout: default +noindex: true +sitemap: false title: Migration guide, v2 to v3 rightmenu: false permalink: /migration-guide-javalin-2-to-3 diff --git a/pages/docs/migration-guide-3-4.md b/pages/docs/migration-guide-3-4.md index eb480f2..f2db653 100644 --- a/pages/docs/migration-guide-3-4.md +++ b/pages/docs/migration-guide-3-4.md @@ -1,5 +1,7 @@ --- layout: default +noindex: true +sitemap: false title: Migration guide, v3 to v4 rightmenu: false permalink: /migration-guide-javalin-3-to-4 diff --git a/pages/docs/migration-guide-4-5.md b/pages/docs/migration-guide-4-5.md index 30192d8..8c84f64 100644 --- a/pages/docs/migration-guide-4-5.md +++ b/pages/docs/migration-guide-4-5.md @@ -1,5 +1,7 @@ --- layout: default +noindex: true +sitemap: false title: Migration guide, v4 to v5 rightmenu: false permalink: /migration-guide-javalin-4-to-5 diff --git a/pages/docs/migration-guide-6-7.md b/pages/docs/migration-guide-6-7.md index d00067b..506236b 100644 --- a/pages/docs/migration-guide-6-7.md +++ b/pages/docs/migration-guide-6-7.md @@ -3,6 +3,7 @@ layout: default title: Migration guide, v6 to v7 rightmenu: false permalink: /migration-guide-javalin-6-to-7 +description: "How to migrate from Javalin 6 to Javalin 7. Covers the upfront config model, Java 17 minimum, Jetty 12, and all breaking API changes." ---

Javalin 6 to 7 migration guide

@@ -612,7 +613,7 @@ override fun onStart(state: JavalinState) { ## Additional changes It's hard to keep track of everything, but you can look at the -[full commit log](https://github.com/javalin/javalin/compare/javalin-parent-{{site.javalinSixVersion}}...javalin-parent-7.0.0-alpha.4) +[full commit log](https://github.com/javalin/javalin/compare/javalin-parent-{{site.javalinSixVersion}}...javalin-parent-7.0.0) between the last 6.x version and 7.0.0. If you run into something not covered by this guide, please edit this page on GitHub! diff --git a/pages/download.md b/pages/download.md index 5c27fc1..b062171 100644 --- a/pages/download.md +++ b/pages/download.md @@ -3,6 +3,7 @@ layout: default title: Download rightmenu: false permalink: /download +description: "Download Javalin. Maven and Gradle dependency snippets for the latest version." --- {% include notificationBanner.html %} diff --git a/pages/for-educators.md b/pages/for-educators.md index 562311d..f0f76f9 100644 --- a/pages/for-educators.md +++ b/pages/for-educators.md @@ -3,6 +3,7 @@ layout: default title: For educators rightmenu: false permalink: /for-educators +description: "Why Javalin is ideal for programming courses, demos, and prototypes. Simple setup, minimal boilerplate, great for teaching." ---

For educators

diff --git a/pages/news.md b/pages/news.md index 2255cb1..2257cce 100644 --- a/pages/news.md +++ b/pages/news.md @@ -3,6 +3,7 @@ layout: default title: News rightmenu: false permalink: /news/ +description: "Javalin release notes, announcements, and changelog." --- {% include notificationBanner.html %} diff --git a/pages/plugins.md b/pages/plugins.md index ec3c65b..795169a 100644 --- a/pages/plugins.md +++ b/pages/plugins.md @@ -3,6 +3,7 @@ layout: default title: Plugin store rightmenu: false permalink: /plugins/ +description: "Javalin plugins β€” CORS, OpenAPI, rendering, SSL, Micrometer, GraphQL, JavalinVue, and more." --- @@ -29,7 +30,7 @@ please report it on GitHub (link in plugin card). " bundled="false" author="dzikoysk" - docsUrl="https://github.com/javalin/javalin-openapi#readme" + docsUrl="https://javalin.github.io/javalin-openapi/" ratingIssueNr="133" %} diff --git a/pages/plugins/cors.md b/pages/plugins/cors.md index 5ebe07e..91d34a2 100644 --- a/pages/plugins/cors.md +++ b/pages/plugins/cors.md @@ -3,6 +3,7 @@ layout: default title: CORS plugin documentation rightmenu: true permalink: /plugins/cors +description: "Configure Cross-Origin Resource Sharing (CORS) in Javalin using the built-in CORS plugin." ---
diff --git a/pages/plugins/devlogging.md b/pages/plugins/devlogging.md index 00198c0..375827a 100644 --- a/pages/plugins/devlogging.md +++ b/pages/plugins/devlogging.md @@ -3,6 +3,7 @@ layout: default title: DevLoggingPlugin documentation rightmenu: false permalink: /plugins/devlogging +description: "Pretty-print incoming requests and responses in development using the Javalin DevLogging plugin." ---

DevLoggingPlugin

diff --git a/pages/plugins/graphql.md b/pages/plugins/graphql.md index b8976c2..49b7543 100644 --- a/pages/plugins/graphql.md +++ b/pages/plugins/graphql.md @@ -3,6 +3,7 @@ layout: default title: GraphQL documentation rightmenu: true permalink: /plugins/graphql +description: "Add a GraphQL endpoint to a Javalin application using the GraphQL plugin." ---
diff --git a/pages/plugins/how-to.md b/pages/plugins/how-to.md index 8ba05ab..1f66470 100644 --- a/pages/plugins/how-to.md +++ b/pages/plugins/how-to.md @@ -3,6 +3,7 @@ layout: default title: How to write a Javalin plugin rightmenu: true permalink: /plugins/how-to +description: "How to create custom plugins for Javalin. Covers the plugin API, lifecycle hooks, and registration." ---
@@ -17,12 +18,9 @@ permalink: /plugins/how-to

About Plugins

-In Javalin 6, the plugin system was completely rewritten. The new system is much more powerful and flexible, -but it's also a bit more complicated. Our hope is that the new system will ensure that all plugins -follow the same pattern, so that it will be easier for end users to use the plugins. +Javalin's plugin system enforces a consistent API so that end users can rely on the same patterns across all plugins. -This how-to guide will walk you through the plugin system, -and how to write your own plugins. +This how-to guide will walk you through the plugin system and how to write your own plugins. ## What is a plugin? A plugin is a piece of code that can be added to Javalin to extend Javalin's functionality. @@ -40,10 +38,10 @@ intimidating, but we will walk you through it step by step. There are also sever abstract class Plugin(userConfig: Consumer? = null, defaultConfig: CONFIG? = null) { /** Initialize properties and access configuration before any handler is registered. */ - open fun onInitialize(config: JavalinConfig) {} + open fun onInitialize(state: JavalinState) {} /** Called when the plugin is applied to the Javalin instance. */ - open fun onStart(config: JavalinConfig) {} + open fun onStart(state: JavalinState) {} /** Checks if plugin can be registered multiple times. */ open fun repeatable(): Boolean = false @@ -66,7 +64,7 @@ which is the type of the configuration object that you want to use. If you don't use a configuration object, you can use `Void`/`Unit` as the type parameter. ### The `ContextPlugin` class -If you want to add functionality to the `Context` class, you can extend the `ContextExtension` class, +If you want to add functionality to the `Context` class, you can extend the `ContextPlugin` class, which in turn extends the `Plugin` class. This class has a generic type parameter `EXTENSION`, in addition to the `CONFIG` type parameter from the `Plugin` class: @@ -81,7 +79,7 @@ abstract class ContextPlugin( } ``` -The `ContextExtension` class has a function `createExtension`, which is called when the +The `ContextPlugin` class has a function `createExtension`, which is called when the user calls `ctx.with(Plugin::class)`. This function should return an instance of the extension class. It also overrides the `repeatable` function to always returns `false`. This is necessary because context extensions are keyed by class, so you can only have one @@ -107,13 +105,11 @@ Let's create a plugin named `Ratey` that does rate limiting: class Ratey extends Plugin { int counter; @Override - public void onInitialize(JavalinConfig config) { - config.router.mount(router -> { - router.before(ctx -> { - if (counter++ > 100) { - throw new TooManyRequestsResponse(); - } - }); + public void onStart(JavalinState state) { + state.routes.before(ctx -> { + if (counter++ > 100) { + throw new TooManyRequestsResponse(); + } }); } } @@ -122,12 +118,10 @@ class Ratey extends Plugin { class Ratey : Plugin() { var counter = 0 - override fun onInitialize(config: JavalinConfig) { - config.router.mount { router -> - router.before { ctx -> - if (counter++ > 100) { - throw TooManyRequestsResponse() - } + override fun onStart(state: JavalinState) { + state.routes.before { ctx -> + if (counter++ > 100) { + throw TooManyRequestsResponse() } } } @@ -149,7 +143,7 @@ val app = Javalin.create { config -> {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} -This will register the plugin. The `onInitialize` function will be called when Javalin is initialized, +This will register the plugin. The `onStart` function will be called when Javalin starts, so our rate-limiting code will be executed for every request. This plugin is currently quite terrible, as it will rate-limit all requests, and it has a hardcoded limit of 100 requests. Let's make it a bit more flexible by adding a config. @@ -169,13 +163,11 @@ class Ratey extends Plugin { // the Ratey.Config class is the conf public int limit = 1; } @Override - public void onInitialize(JavalinConfig config) { - config.router.mount(router -> { - router.before(ctx -> { - if (counter++ > pluginConfig.limit) { // we can access the config through the pluginConfig field - throw new TooManyRequestsResponse(); - } - }); + public void onStart(JavalinState state) { + state.routes.before(ctx -> { + if (counter++ > pluginConfig.limit) { // we can access the config through the pluginConfig field + throw new TooManyRequestsResponse(); + } }); } } @@ -190,12 +182,10 @@ class Ratey(userConfig: Consumer) : Plugin(userConfig, Con var limit = 1 } - override fun onInitialize(config: JavalinConfig) { - config.router.mount { router -> - router.before { ctx -> - if (counter++ > pluginConfig.limit) { // we can access the config through the pluginConfig field - throw TooManyRequestsResponse() - } + override fun onStart(state: JavalinState) { + state.routes.before { ctx -> + if (counter++ > pluginConfig.limit) { // we can access the config through the pluginConfig field + throw TooManyRequestsResponse() } } } @@ -239,7 +229,7 @@ For this example, we want to be able to limit the number of requests per user, a Instead of a before-handler, we will create an extension to the `Context` class, which we will let users call with `ctx.with(Ratey::class)`. This will return an instance of our extension class. -Let's extend the `ContextExtension` class in our `Ratey` plugin and add a `createExtension` function: +Let's extend the `ContextPlugin` class in our `Ratey` plugin and add a `createExtension` function: {% capture java %} class Ratey extends ContextPlugin { @@ -304,40 +294,34 @@ class Ratey(userConfig: Consumer) : ContextPlugin { config.registerPlugin(new Ratey(rateyConfig -> { rateyConfig.limit = 100_000; })); -}); - -// use in handler -app.get("/cheap-endpoint", ctx -> { - ctx.with(Ratey.class).tryConsume(1); - ctx.result("Hello cheap world!"); -}); -app.get("expensive-endpoint", ctx -> { - ctx.with(Ratey.class).tryConsume(100); - ctx.result("Hello expensive world!"); -}); + config.routes.get("/cheap-endpoint", ctx -> { + ctx.with(Ratey.class).tryConsume(1); + ctx.result("Hello cheap world!"); + }); + config.routes.get("/expensive-endpoint", ctx -> { + ctx.with(Ratey.class).tryConsume(100); + ctx.result("Hello expensive world!"); + }); +}).start(7070); {% endcapture %} {% capture kotlin %} -// register val app = Javalin.create { config -> config.registerPlugin(Ratey { rateyConfig -> rateyConfig.limit = 100_000 }) -} - -// use in handler -app.get("/cheap-endpoint") { ctx -> - ctx.with(Ratey::class).tryConsume(1) - ctx.result("Hello cheap world!") -} -app.get("expensive-endpoint") { ctx -> - ctx.with(Ratey::class).tryConsume(100) - ctx.result("Hello expensive world!") -} + config.routes.get("/cheap-endpoint") { ctx -> + ctx.with(Ratey::class).tryConsume(1) + ctx.result("Hello cheap world!") + } + config.routes.get("/expensive-endpoint") { ctx -> + ctx.with(Ratey::class).tryConsume(100) + ctx.result("Hello expensive world!") + } +}.start(7070) {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} diff --git a/pages/plugins/javalinvue.md b/pages/plugins/javalinvue.md index aef80b7..b157028 100644 --- a/pages/plugins/javalinvue.md +++ b/pages/plugins/javalinvue.md @@ -3,6 +3,7 @@ layout: default title: JavalinVue Plugin rightmenu: true permalink: /plugins/javalinvue +description: "Server-driven Vue.js component loading in Javalin without a build step, using the JavalinVue plugin." ---
@@ -46,7 +47,7 @@ and serves it all as one big HTML file. You start by creating a layout file:
- @routeComponent + @routeComponent
diff --git a/pages/plugins/rendering.md b/pages/plugins/rendering.md index 7f69668..84159da 100644 --- a/pages/plugins/rendering.md +++ b/pages/plugins/rendering.md @@ -3,17 +3,19 @@ layout: default title: Javalin Rendering rightmenu: false permalink: /plugins/rendering +description: "Template rendering in Javalin via modular javalin-rendering-{engine} modules. Supports Freemarker, Thymeleaf, Mustache, JTE, Pebble, Handlebars, Velocity, and CommonMark." ---

Javalin rendering

-The `javalin-rendering` artifact is an optional module for the Javalin web framework that -provides a simple way to render HTML using popular template engines. -The `javalin-rendering` artifact includes default implementations for several template engines, -including JTE, Mustache, Velocity, Pebble, Handlebars, and Thymeleaf, but you -also have to add the dependency for the template engine you want to use. +The `javalin-rendering` module provides optional template engine support for Javalin. +Each template engine has its own artifact that bundles the engine dependency, so you only +need to add one dependency to get started. ## Adding dependencies + +Pick the artifact for the template engine you want to use: +
  • Maven
  • @@ -23,36 +25,31 @@ also have to add the dependency for the template engine you want to use. ~~~markup io.javalin - javalin-rendering + javalin-rendering-{engine} {{site.javalinversion}} - - - - - ~~~
-~~~java -implementation("io.javalin:javalin-rendering:{{site.javalinversion}}") -// template engine dependency +~~~kotlin +implementation("io.javalin:javalin-rendering-{engine}:{{site.javalinversion}}") ~~~
-### Included template engines -The `javalin-rendering` artifact includes default implementations for several template engines: +### Available modules +Replace `{engine}` with the name of the template engine you want to use: | --- | --- | -| **Freemarker** |  βžœ  [https://freemarker.apache.org](https://freemarker.apache.org) | -| **JTE** |  βžœ  [https://jte.gg/](https://jte.gg) | -| **Mustache** |  βžœ  [https://github.com/spullara/mustache.java](https://github.com/spullara/mustache.java) | -| **Pebble** |  βžœ  [https://pebbletemplates.io/](https://pebbletemplates.io) | -| **Thymeleaf** |  βžœ  [https://www.thymeleaf.org/](https://www.thymeleaf.org) | -| **Velocity** |  βžœ  [https://velocity.apache.org/](https://velocity.apache.org) | -| **Commonmark** |  βžœ  [https://github.com/commonmark/commonmark-java](https://github.com/commonmark/commonmark-java) | +| `javalin-rendering-commonmark` |  βžœ  [https://github.com/commonmark/commonmark-java](https://github.com/commonmark/commonmark-java) | +| `javalin-rendering-freemarker` |  βžœ  [https://freemarker.apache.org](https://freemarker.apache.org) | +| `javalin-rendering-handlebars` |  βžœ  [https://github.com/jknack/handlebars.java](https://github.com/jknack/handlebars.java) | +| `javalin-rendering-jte` |  βžœ  [https://jte.gg/](https://jte.gg) | +| `javalin-rendering-mustache` |  βžœ  [https://github.com/spullara/mustache.java](https://github.com/spullara/mustache.java) | +| `javalin-rendering-pebble` |  βžœ  [https://pebbletemplates.io/](https://pebbletemplates.io) | +| `javalin-rendering-thymeleaf` |  βžœ  [https://www.thymeleaf.org/](https://www.thymeleaf.org) | +| `javalin-rendering-velocity` |  βžœ  [https://velocity.apache.org/](https://velocity.apache.org) | ## Using the plugin All the template engines look for templates/markdown files in `src/resources/templates`. @@ -99,65 +96,5 @@ Javalin.create { config -> {% endcapture %} {% include macros/docsSnippet.html java=java kotlin=kotlin %} -The configuration classes are not from Javalin, but from the template engine you are using, +The configuration classes are not from Javalin, but from the template engine you are using, so please consult the documentation for that particular template engine to learn how to use them. - -## Recreating the old JavalinRenderer -Older versions of Javalin had a `JavalinRenderer` class that was used to render templates. -This class was able to render templates based on the file extension. - -You can recreate this class like this: - -{% capture java %} -class JavalinRenderer implements FileRenderer { - private Map renderers = new HashMap<>(); - public JavalinRenderer register(String extension, FileRenderer renderer) { - renderers.put(extension, renderer); - return this; - } - - @Override - public String render(String filePath, Map model, Context context) { - String extension = filePath.substring(filePath.lastIndexOf(".") + 1); - return renderers.get(extension).render(filePath, model, context); - } -} -{% endcapture %} -{% capture kotlin %} -class JavalinRenderer : FileRenderer { - private val renderers = HashMap() - fun register(extension: String, renderer: FileRenderer): JavalinRenderer { - renderers[extension] = renderer - return this - } - - - override fun render(filePath: String, model: Map, context: Context): String { - val extension = filePath.substring(filePath.lastIndexOf(".") + 1) - return renderers[extension]!!.render(filePath, model, context) - } -} -{% endcapture %} -{% include macros/docsSnippet.html java=java kotlin=kotlin %} - -You can then register it like any other renderer: - -{% capture java %} -Javalin.create(config -> { - config.fileRenderer( - new JavalinRenderer() - .register("mustache", new JavalinMustache()) - .register("jte", new JavalinJte()) - ); -}); -{% endcapture %} -{% capture kotlin %} -Javalin.create { config -> - config.fileRenderer( - JavalinRenderer() - .register("mustache", JavalinMustache()) - .register("jte", JavalinJte()) - ) -} -{% endcapture %} -{% include macros/docsSnippet.html java=java kotlin=kotlin %} diff --git a/pages/plugins/routeoverview.md b/pages/plugins/routeoverview.md index ca3280d..542ce31 100644 --- a/pages/plugins/routeoverview.md +++ b/pages/plugins/routeoverview.md @@ -3,6 +3,7 @@ layout: default title: RouteOverview documentation rightmenu: false permalink: /plugins/routeoverview +description: "Expose a JSON/HTML overview of all registered routes in a Javalin application using the RouteOverview plugin." ---

RouteOverview Plugin

diff --git a/pages/plugins/ssl-helpers.md b/pages/plugins/ssl-helpers.md index c849735..5662678 100644 --- a/pages/plugins/ssl-helpers.md +++ b/pages/plugins/ssl-helpers.md @@ -3,6 +3,7 @@ layout: default title: SSL Plugin rightmenu: true permalink: /plugins/ssl-helpers +description: "Configure HTTPS, HTTP/2, and mutual TLS in Javalin using the SSL plugin." ---
diff --git a/pages/tutorials.md b/pages/tutorials.md index ab9dc55..4c32340 100644 --- a/pages/tutorials.md +++ b/pages/tutorials.md @@ -2,6 +2,7 @@ layout: default title: Tutorials permalink: /tutorials/ +description: "Javalin tutorials β€” step-by-step guides for REST APIs, WebSockets, testing, authentication, and more." --- {% include notificationBanner.html %} diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..b040834 --- /dev/null +++ b/robots.txt @@ -0,0 +1,7 @@ +--- +--- +User-agent: * +Allow: / + +Sitemap: {{ site.url }}/sitemap.xml +