A minimal web app in Java using some common tools.
Project Structure
1 2 3 4 5 6 7 8 9 10 my-app/ ├── javalin-server/ │ ├── App.java │ └── build.gradle ├── k6-tests/ │ └── load-test.js ├── playwright-tests/ │ ├── test.js │ └── package.json └── Makefile
The Web App
For this extremely lean application, Javalin hits the sweet spot: minimal code, active maintenance, and zero ceremony. One file, CRUD endpoints, done.
Alternatives: Spring Boot, Helidon.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import io.javalin.Javalin;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;import java.util.UUID;public class App { static final Map<String, Item> items = new ConcurrentHashMap <>(); record Item (String id, String name, String description) {} public static void main (String[] args) { var app = Javalin.create().start(8080 ); System.out.println("CRUD server on http://localhost:8080" ); app.post("/items" , ctx -> { var id = UUID.randomUUID().toString(); items.put(id, new Item (id, ctx.formParam("name" ), ctx.formParam("description" ))); ctx.status(201 ).json(items.get(id)); }); app.get("/items" , ctx -> ctx.json(items.values())); app.get("/items/{id}" , ctx -> { var item = items.get(ctx.pathParam("id" )); if (item == null ) ctx.status(404 ); else ctx.json(item); }); app.put("/items/{id}" , ctx -> { var id = ctx.pathParam("id" ); if (!items.containsKey(id)) { ctx.status(404 ); return ; } items.put(id, new Item (id, ctx.formParam("name" ), ctx.formParam("description" ))); ctx.json(items.get(id)); }); app.delete("/items/{id}" , ctx -> { ctx.status(items.remove(ctx.pathParam("id" )) == null ? 404 : 204 ); }); } }
Build with Gradle
Think of build.gradle as a Makefile for Java projects. Here’s what each section does:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 plugins { id 'application' } repositories { mavenCentral() } dependencies { implementation 'io.javalin:javalin:6.3.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0' } application { mainClass = 'App' } sourceSets { main { java { srcDirs = ['.' ] } } }
Load Testing with k6
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import http from 'k6/http' ;import { check, sleep } from 'k6' ;export const options = { stages : [ { duration : '5s' , target : 5 }, { duration : '10s' , target : 5 }, { duration : '5s' , target : 0 }, ], }; const BASE = 'http://localhost:8080' ;export default function ( ) { const payload = { name : 'Test' , description : 'Test item' }; const create = http.post (`${BASE} /items` , payload); check (create, { 'POST /items 201' : (r ) => r.status === 201 }); const id = JSON .parse (create.body ).id ; const get = http.get (`${BASE} /items/${id} ` ); check (get, { 'GET /items/{id} 200' : (r ) => r.status === 200 }); const list = http.get (`${BASE} /items` ); check (list, { 'GET /items 200' : (r ) => r.status === 200 }); const update = http.put (`${BASE} /items/${id} ` , { name : 'Updated' , description : 'Updated desc' }); check (update, { 'PUT /items/{id} 200' : (r ) => r.status === 200 }); const del = http.del (`${BASE} /items/${id} ` ); check (del, { 'DELETE /items/{id} 204' : (r ) => r.status === 204 }); sleep (1 ); }
The test exercises the full CRUD cycle: create an item, read it back, list all items, update it, then delete it. The stages ramp up to 5 virtual users, hold, then ramp down. The sleep(1) simulates realistic user pacing between requests.
API Testing with Playwright
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 const { chromium, request, expect } = require ('@playwright/test' );(async () => { const apiContext = await request.newContext ({ baseURL : 'http://localhost:8080' }); const item1 = await apiContext.post ('/items' , { form : { name : 'Widget' , description : 'A useful widget' } }); const item2 = await apiContext.post ('/items' , { form : { name : 'Gadget' , description : 'A fancy gadget' } }); console .log ('Created:' , await item1.json ()); console .log ('Created:' , await item2.json ()); await apiContext.dispose (); const browser = await chromium.launch ({ headless : false }); const page = await browser.newPage (); await page.goto ('http://localhost:8080/items' ); console .log ('Viewing items list...' ); await page.waitForLoadState ('networkidle' ); const body = page.locator ('body' ); await expect (body).toContainText ('Widget' ); await expect (body).toContainText ('A useful widget' ); await expect (body).toContainText ('Gadget' ); await expect (body).toContainText ('A fancy gadget' ); console .log ('✓ Both items found on page with correct content' ); await browser.close (); console .log ('Test completed successfully' ); })();
Playwright tests both the API and the GUI. First, it creates records via the HTTP API. Then it launches a browser, navigates to the items page, and asserts that the created items are visible using expect().toContainText().
Makefile
1 2 3 4 5 6 7 8 9 10 .PHONY : server k6 playwrightserver: cd javalin-server && gradle run k6: k6 run k6-tests/load-test.js playwright: cd playwright-tests && node test.js