NASEBANAL Quickstarts − Why We Built It, and the Scenarios It Ships With

Table of contents
  1. Why we built NASEBANAL Quickstarts
  2. NASEBANAL Quickstarts' structure and features
  3. The scenarios
  4. Test result reports
  5. Summary

We built NASEBANAL Quickstarts for use in training as part of NASEBANAL's consulting services, and publish it as open source. This post covers why we built it, how it fits together, and its seven scenarios, along with the key features of the OSS tools they use, the processing flow, and what we actually got when we ran them. The step-by-step instructions live in the demo app's built-in docs and the repository README, so here we stick to *what you can try, and what we were able to confirm*.


Why we built NASEBANAL Quickstarts

AI coding agents are spreading fast, and handing off implementation work by describing requirements to them is becoming routine. But hand an AI too large a requirement at once, and it stumbles the same way a person does — details get missed, scope gets muddled. Breaking requirements into smaller, clearly-bounded pieces is, I think, part of why architectures like microservices are getting a fresh look.

At the same time, microservices bring in concepts a monolith never made you think about: an API gateway, event streaming, service discovery, contract testing, API mocking, an identity provider, secret management, observability. Adopting the right tool for each matters. But the more tools you add, the more each one's install steps, start/stop commands, and admin-console access diverge — and that's not something anyone can keep memorized.

I've also noticed that software quality and architecture practices vary wildly by vendor, and even project to project — often boiled down to whatever one person happens to know. Standardizing the test strategy, the tooling, and how test results get managed is what actually raises the floor on software quality overall. To absorb that "every tool does it differently" problem, we built NASEBANAL Quickstarts (repository name: nb-quickstarts), an open-source tool NASEBANAL publishes.

NASEBANAL Quickstarts' structure and features

Every module is driven by the same Docker Compose call — make <module>:up / make <module>:down — and any tool with an admin console opens it straight from make <module>:open. Once you're done, make <module>:down cleans up after itself, so there's no leftover cruft to worry about. Each technology in the NASEBANAL Stack is carved out as its own module.

The overall picture comes in the two diagrams on the Overview page of the demo app's built-in docs.

The big picture — apps-network and the modules around it

The first is the overall diagram. Centered on the test-target apps (frontend, backend, MySQL), Kong in front of it, Kafka for async ingestion, Keycloak for identity, Vault for secrets, and Observability for monitoring all sit on the same network (apps-network), and you can combine just the ones you need. Arrows point from the caller to what it calls, and dashed lines are integrations that are off by default.

Overall architecture diagram of NASEBANAL Quickstarts: centered on frontend, backend and MySQL inside apps-network, with Kong in front of the frontend (its upstream can be switched to Specmatic or Microcks mocks), Kafka feeding Locust's load into the backend through kafka-bridge, Keycloak providing JWT public keys and Vault issuing DB users, and Observability receiving traces, metrics and logs over OTLPOverall architecture diagram of NASEBANAL Quickstarts: centered on frontend, backend and MySQL inside apps-network, with Kong in front of the frontend (its upstream can be switched to Specmatic or Microcks mocks), Kafka feeding Locust's load into the backend through kafka-bridge, Keycloak providing JWT public keys and Vault issuing DB users, and Observability receiving traces, metrics and logs over OTLP

The MCP access path — built-in /mcp and agentgateway

The second diagram shows how an MCP client, such as an AI agent, reaches the backend (the parts involved in Scenarios 5 to 7). The MCP client skips the Frontend and goes through the backend's built-in /mcp, or optionally through agentgateway (dashed, like Kong), which builds MCP tools from the OpenAPI contract and calls the REST API. The JWT from the Keycloak login is verified at the backend, and the backend gets its DB credential from Vault.

The MCP access path. An MCP client reaches the backend without going through the Frontend, either directly through its built-in /mcp or, optionally, through agentgateway, which builds MCP tools from OpenAPI and calls the backend's REST API. The client logs in at Keycloak to get a JWT, the backend fetches Keycloak's public keys to verify it, and gets its DB credential from Vault to connect to MySQLThe MCP access path. An MCP client reaches the backend without going through the Frontend, either directly through its built-in /mcp or, optionally, through agentgateway, which builds MCP tools from OpenAPI and calls the backend's REST API. The client logs in at Keycloak to get a JWT, the backend fetches Keycloak's public keys to verify it, and gets its DB credential from Vault to connect to MySQL

The demo app, and its built-in docs

The test-target apps module is a simple accounting-ledger app: a FastAPI backend serving REST, GraphQL and MCP, and a Next.js frontend. The data follows an event-sourcing approach — an account's balance is derived as the sum of the transactions appended one by one. Tools like Playwright and Locust are ready to try against it right away.

The NASEBANAL Quickstarts demo app's landing page, showing an example run of make apps:up and make playwright:test, and the list of endpoints available once started

Start it with make apps:up and the demo app ships with its own docs (/docs). You can read each scenario's steps right next to the running app, with the commands and what "working" should look like.

The scenarios

There are seven scenarios, numbered the same way as in the in-app docs. Scenario 1 verifies the demo app itself with the test tools; Scenarios 2 to 7 each take one module from the diagrams above and wire it into the same running apps to see how it behaves. Below, each scenario comes with what it does, the key features of the OSS tools it uses, and what we actually confirmed.

Scenario 1: Verify the demo app

Verify the operation of the demo app itself, the same running apps that every other scenario builds on, with the test tools that ship in the box. Each tool has its own make command and leaves an HTML report behind; the report screens are shown in "Test result reports" below.

  • pytest and Vitest — unit tests, run without a browser. pytest swaps MySQL for an in-memory database, so the backend tests need no other service; Vitest covers the frontend's logic (the API client, the OIDC PKCE flow and the backend resolver)
  • Playwright — end-to-end tests that drive the real frontend in a browser against the real backend and MySQL
  • Specmatic — contract tests: it reads the hand-written openapi.yaml, sends requests to the running backend and reports coverage per path, method and response code, including the error responses
  • Microcks — a second provider check: it sends the examples written in openapi.yaml to the real backend and checks each response against the example and the schema
  • Locust and OWASP ZAP — load tests and security scans

What we confirmed: the 22 pytest tests, 20 Vitest tests, 9 Playwright tests and all 18 Specmatic scenarios (100% API coverage) passed. Microcks was the one that found something. Of the 9 examples it ran, 6 passed and 3 failed, and the failures were findings rather than test defects. First, createdAt is declared as format: date-time in the contract, but the backend answers 2026-09-23T07:00:26 with no time zone offset, which is not valid RFC 3339. Specmatic passes all 18 scenarios against the same backend, whereas Microcks flags the format: a genuine gap between the contract and the implementation. Second, POST /auth/login has a response example named bad_credentials but no request example with that key, so Microcks sends an empty body and gets a 422 instead of the 401 the example expects. That one is a gap in the examples, not a backend defect.

Scenario 2: Switch to Kong

Route the frontend through the Kong API gateway, then repoint the Gateway Service apps_backend from the real backend to Specmatic's stub or Microcks' mock. The frontend code doesn't change at all, yet it's now talking to a mock built from the same contract.

  • Kong — puts authentication, rate limiting and routing in one gateway in front of the backend, and lets you change where traffic goes at runtime without touching application code. In Kong Manager, the admin UI, editing a service's Host / Port / Path and saving is all it takes to switch. This scenario runs Kong in DB mode, keeping its configuration in PostgreSQL
  • Specmatic — generates a mock server (a stub) straight from the OpenAPI contract. The same contract can verify the real backend too, so the mock and the implementation can't quietly drift apart
  • Microcks — import the contract once and get a running mock with example responses, so consumers such as the frontend can develop before the real API is ready
Kong Manager's Edit Gateway Service form for apps_backend, showing Host backend, Port 8080 and an empty Path

What we confirmed: we ran the exact same curl http://localhost:8000/api/accounts/balances before and after the switch. Before it, the real ledger data came back (for example, Cash at a balance of 120,000). After repointing Kong at Specmatic's stub, the same URL returned a different value that matches the schema but is randomly generated. That difference shows the target was swapped through a runtime configuration change alone, with no frontend edit. One limit: Microcks can't mock POST /accounts, because an OpenAPI example has no way to carry the real bearer token it needs (read endpoints work fine).

Scenario 3: Switch to Kafka

Buffer writes in Kafka and have kafka-bridge (a small consumer that reads the Kafka topic and sends each event to the backend as a POST /accounts) forward them one at a time, then compare the same load hitting REST directly versus going through Kafka.

  • Kafka — producers write to a durable log at their own pace while the consumer drains it at a steady rate. It absorbs bursts, and because messages are retained until consumed, a backend outage only delays processing instead of dropping data. Producers and consumers don't need to know about each other, so either side can be added, scaled or replaced independently
  • Locust — load scenarios are written in Python and run from the web UI or from the command line, as in make locust:test. It also supports distributed runs, where a Locust master assigns users to several workers, and leaves an HTML report behind

What surprised us: we sent the same write (POST /accounts) for 60 seconds with 600 users and zero think time. The backend ran on a single laptop, deliberately left in a weak, untuned configuration.

  • Direct REST: 46 of 58 writes failed (79%). The connection pool was exhausted under the overload, and responses degraded to seconds
  • Via Kafka: it absorbed 1,241,297 events with zero failures, with responses steady in the millisecond range throughout
Line chart comparing cumulative request counts on a log scale for 600 users over 60 seconds. Direct REST plateaus early, with 46 of 58 requests failing (79%); via Kafka sends 1,241,297 requests with zero failures, growing at a nearly constant slope

That said, Kafka didn't make the load disappear. kafka-bridge forwards to the backend at its own pace, so what's really happening is that the burst gets absorbed and the shape in which it reaches the backend changes. In fact, looking at the consumer group after a 300-user, 40-second run, about 920,000 of the roughly 1.66 million events written had not reached the backend yet (the lag). "Written to Kafka" and "applied by the backend" are different things, and the latter takes time. Trade-offs like that eventual consistency and the added operational cost are real — but separating the write intake from the processing capacity changes how a sudden burst is handled entirely, and we were able to confirm that with actual numbers.

Scenario 4: Observability

Send the backend's traces, metrics and logs over OpenTelemetry and view them in Prometheus, Tempo, Loki and Grafana. Watch a load run live in Grafana, all the way to an alert firing through Alertmanager.

  • OpenTelemetry — a vendor-neutral standard for traces and metrics. Instrument once, and you can switch the receiving side (a local stack here, New Relic in production, for example) without touching app code. In this scenario, FastAPI requests and SQLAlchemy queries, HTTP server metrics and application logs are all sent to a Collector over OTLP
  • Prometheus / Tempo / Loki — purpose-built stores for metrics, traces and logs, all open source with no license cost. Prometheus also evaluates the alert rules
  • Grafana — metrics, traces and logs in one place. A log line carries its trace_id, so you can jump from a log to its trace, and from a trace to that span's logs, without copying an ID by hand
  • Alertmanager — turns Prometheus alerts into notifications by grouping, de-duplicating and routing them. It can also mute minor alerts for the same service while a more severe one is firing (inhibition)

The scenario puts three alert rules into Prometheus: BackendHighErrorRatio (5xx above 5%, critical), BackendHighLatencyP95 (p95 latency above 1s, warning) and BackendDbPoolSaturated (15 DB pool connections in use, warning). Each fires only after the condition has held for 30 seconds, so one noisy sample doesn't trigger it.

What we confirmed: we ran the direct-REST load that failed in Scenario 3 (300 users, 40 seconds) again, this time with Grafana open.

  • p95/p99 latency climbed toward the 30-second DB pool timeout, and DB connections in use stayed pinned near the ceiling. Run the same load through Kafka and those panels stay flat
  • On the alerts, the two warnings fired first and the critical 5xx alert followed about a minute later. Once it fired, the inhibit rule kicked in, so the on-call person gets one critical alert instead of three
  • In Loki we could read the cause of the 5xx spike, with its stack trace: the DB pool was exhausted — QueuePool limit of size 5 overflow 10 reached, connection timed out, timeout 30.00
The Grafana dashboard with the 5xx error ratio near 100%, a firing-alerts panel showing BackendHighLatencyP95, BackendDbPoolSaturated and BackendHighErrorRatio, and a Loki log panel full of backend exceptions

Kong's and agentgateway's traces can go to the Collector too. A request that passes through a gateway shows up as a single trace: the gateway's span with the backend's spans, SQL queries included, hanging beneath it.

Scenario 5: Keycloak

Add a real OIDC login to the login page. The backend verifies the JWT Keycloak issued against its public key, so the backend never handles a password.

  • Keycloak — an identity provider for standard OIDC/OAuth 2.0. It handles login, token issuing and SSO so you don't write authentication code yourself. Users, roles and federation with external or enterprise identities are managed in one place, and the same IdP can serve several apps. In this scenario, a fixed realm (nasebanal) is imported on every start, with a demo user and sign-up (user registration) turned on

The sequence follows the same idea as "Sign in with Google" (an Authorization Code flow with PKCE):

  1. Frontend → Keycloak: redirect the user to Keycloak's login (or sign-up) page, with a client_id and a code_challenge
  2. Keycloak: the user signs in here. The app never sees the password
  3. Keycloak → Frontend: redirect back to /auth/callback with a one-time authorization code
  4. Frontend → Keycloak: send the code and the code_verifier to POST /token to exchange them for tokens
  5. Keycloak → Frontend: return an access token (JWT)
  6. Frontend → Backend: call POST /accounts and the like with Authorization: Bearer <JWT>
  7. Backend → Keycloak: only on the first request (or after a key rotation), fetch the public keys (GET /certs, JWKS) and cache them
  8. Backend: verify the signature, issuer and expiry, and return 201 Created, or 401 if any check fails

The backend doesn't query Keycloak per request; it decides by verifying the signature against the public key alone. The frontend's OIDC handling is written by hand, with no OIDC library: a redirect out, a code back, and one fetch.

Keycloak's own Sign in to your account page. The password is typed at Keycloak's URL, not in the app, and the Register link handles sign-up

What we confirmed: make keycloak:verify-apps checks this without a browser. It gets a real token from Keycloak and calls POST /accounts with it.

$ make keycloak:verify-apps
1. Getting a real access token from Keycloak (realm nasebanal, user keycloak-demo)...
2. Calling the real backend's protected POST /accounts with it...
   HTTP 201

$ curl -X POST localhost:8080/accounts ...            # no token
401
$ curl ... -H "Authorization: Bearer <token, last character changed>"
{"detail":"invalid or missing token"}  401
$ curl ... -H "Authorization: Bearer <genuine token>"
201

The genuine token was accepted, while a request with no token and one whose last character was changed so the signature no longer matches were both rejected with 401. The backend trusts a signature that matches Keycloak's public key, not the contents of the token. In the browser, you can also confirm that a new user who signs up at Keycloak can record a transaction right away, without ever being registered with the backend beforehand.

Scenario 6: Vault

Remove the MySQL password from the backend's config and have it request one from Vault at startup. Vault creates a short-lived MySQL user on the spot and drops it when the lease ends.

  • Vault — keeps secrets in one audited store instead of scattering them across .env files, images and CI settings. Access is controlled by policy and token, and every read can be logged. The database secrets engine this scenario uses issues credentials dynamically: instead of handing out a fixed password, it creates a fresh credential on every request and revokes it automatically when the lease (the loan period) ends

The sequence is as follows. Vault holds one privileged MySQL connection (root), configured just once, and the backend holds only a Vault token that is allowed to ask for a credential.

  1. Backend → Vault: request a database credential with GET /v1/database/creds/apps-backend (with X-Vault-Token)
  2. Vault → MySQL: using the root only Vault has, CREATE USER a new user with a random name and password, limited to the demo database
  3. MySQL → Vault: return OK
  4. Vault → Backend: return the username, the password and a lease (1 hour)
  5. Backend → MySQL: connect as that user and run SQL (the backend renews the lease at half the TTL, up to the role's 24-hour ceiling)
  6. Vault → MySQL: when the lease ends or is revoked, DROP USER removes that user

What we confirmed: we checked "it doesn't work without Vault" and "it works with Vault", in that order.

First, with the backend's BACKEND_MYSQL_PASSWORD empty and no Vault, the backend can't start and keeps retrying the login (make vault:prove-needs-vault).

[db] attempt 1/30 failed: (pymysql.err.OperationalError) (1045, "Access denied for user 'demo'@'172.20.0.3' (using password: NO)")
[db] attempt 2/30 failed: ... Access denied for user 'demo' ... (using password: NO)
  GET /accounts/balances -> HTTP 000
  (no answer - the backend never came up)

Next, give it Vault, and the backend's own startup log shows the user Vault issued dynamically (make vault:verify-apps).

Password in the backend's environment: '' (empty on purpose)

[vault] issued a dynamic MySQL user v-token-apps-backe-c9wybUj5oXeKe (lease 3600s) from http://vault:8200/v1/database/creds/apps-backend

[{"name":"","balance":7,"eventCount":1},{"name":"Cash","balance":120034,"eventCount":15}]

With the password in its environment left empty on purpose, the backend connected to MySQL as the v-token-apps-backe-... user Vault issued, and real balances came back. Restart the backend again and a second, different user is created (make vault:db-users lists them in MySQL's own mysql.user). Nobody typed that name or password; Vault generated both. One caveat: the Vault used here is an in-memory dev server, and everything, including its lease records, is gone on vault:down — a setup meant only for trying things on your own machine.

Scenario 7: MCP through agentgateway

Instead of writing MCP code in the backend, stand up a gateway that builds MCP tools from the OpenAPI contract, so AI agents can call them.

  • agentgateway — turns an existing OpenAPI contract into MCP tools with configuration alone. Sending MCP (and A2A) traffic through one gateway makes it a single place for access control, observability and routing of what AI agents can call. Because the tools come from the same contract as everything else, they follow API changes automatically. It also ships a dashboard UI where you can see which route is wired to which backend

The flow is this: an MCP client (Claude Desktop, mcp-inspector and the like) connects to agentgateway's /mcp over Streamable HTTP, gets the tools with tools/list, and when it calls one with tools/call, agentgateway calls the matching REST API on the backend.

agentgateway's dashboard, with Traffic > Routes showing Route 1 wired to the apps backend

What we confirmed: make agentgateway:tools does the MCP handshake by hand and lists what it serves: six tools, one per operation in openapi.yaml, named and described straight from the contract (health, login, list_accounts, create_account, list_balances and get_account). Calling list_balances through the gateway returns the same live data as GET /accounts/balances itself, so it's a real proxy to the running backend, not a static description. create_account needs a real bearer token, just like the REST call does; without the token you get a 401, unless you call login first and pass its token along.

In Grafana and Tempo from Scenario 4, we could also see tools/call and the backend's REST request and SQL query joined into a single trace, so the MCP call and the REST call can be followed in one place.

Test result reports

These are the reports Scenario 1 leaves behind. All of the test tools run with the same make <module>:test feel and leave an HTML report behind, and the same running apps is tested from every angle: unit, end to end, contract, load and security. The latest results are below.

LayerToolWhat it checksResult
Unit (backend)pytestAccounts and balances, password login, profile, Keycloak JWT validation (including forged and tampered tokens)22 tests
Unit (frontend)VitestThe API client, the OIDC PKCE flow, and the resolver that picks the backend to forward to (round robin, fallback to the next instance)20 tests
End to endPlaywrightLogin and logout, profile, a wrong password and the resolver API, in a real browser9 tests
ContractSpecmaticWhether the real backend honors openapi.yaml18 scenarios, 100% API coverage
Contract (second check)MicrocksThe examples in openapi.yaml, sent to the real backend and checked against the schema9 examples: 6 pass, 3 findings
LoadLocustHTTP, GraphQL and MySQL, overload, and the same overload via KafkaResults of Scenarios 3 and 4
SecurityOWASP ZAPA passive scan of the frontend, and an OpenAPI-driven scan of every backend routeNo High or Medium alerts

Below are the report screens these commands actually generated.

Unit tests: pytest and Vitest

pytest swaps MySQL for an in-memory database, so it runs anywhere in under a second (22 tests in 570 ms). Vitest tests the frontend's logic without a browser: the API client, the login's PKCE handling, and the server-side resolver that picks the backend to forward to. Neither needs apps running, so they're the quickest to try first.

pytest-html report: an environment table and a summary listing the backend tests, all passed
Vitest HTML report: 20 passed, 0 failed, with the test tree grouped by file on the left

End to end and contract: Playwright and Specmatic

Playwright drives the real frontend in a browser against the real backend and MySQL. On a failure, the trace (screenshots, network, console) is one click away.

Specmatic reads openapi.yaml, sends real requests to the running backend, and reports coverage per path, method and response code, including the error responses (401, 404, 422) the happy path never reaches. That openapi.yaml is written by hand, not generated from the code, so it can genuinely disagree with the implementation, which is what makes checking it worthwhile.

Playwright HTML report listing the 9 end-to-end tests as passed, with durations
Specmatic contract test results: 100% API coverage, 18 successes, and a table of every path, method and response code marked Covered

Contract, a second check: Microcks

make microcks:test imports the contract the backend serves at /openapi.json (it is openapi.yaml, verbatim) and asks Microcks to run its conformance test. For every named example in the contract, Microcks builds the request, sends it to the real backend, and checks the status code and the body against the example's response and the operation's schema. Nothing is generated: what is tested is exactly what the contract's examples specify. The findings are described under Scenario 1 above.

Microcks UI, the run's page for the backend: 6 of the 9 examples passed, and the three failing ones are in POST /auth/login, GET /accounts and GET /accounts/{account_id}
Microcks UI, GET /accounts/{account_id} opened: the request Microcks sent, and the backend's 200 response with createdAt 2026-09-23T07:00:26, flagged as not a valid RFC 3339 date-time

Specmatic and Microcks read the same openapi.yaml, and both can test the real backend against it. What we saw of how they differ:

SpecmaticMicrocks
ImplementationKotlin; MIT (open-source edition), with the Studio and Insights screens as paid productsJava; Apache-2.0
Run asA command (CLI or Docker image)A server (Docker Compose or Kubernetes)
Test cases built fromThe spec file's definitions, plus any examples suppliedThe named examples in the spec file; operations without an example are not run
createdAt without a time zone offsetNot detectedDetected
MockOne mock server per spec as set up here; generated data; no UI in the open-source editionSpecs imported into a running server; the spec's own examples; a UI and an API catalog

Load and security: Locust and ZAP

Locust reports response-time percentiles and failures per endpoint (the run below is a short 10-user, 20-second one: 97 requests, 0 failures). The comparisons in Scenarios 3 and 4 are based on this report and its stats history.

ZAP scans for vulnerabilities. The OpenAPI-driven scan (api-scan) sends real attack payloads to every backend route, so it only ever targets this demo's apps. In this scan of the backend, across 27 endpoints there were no High or Medium alerts (2 Low and 5 Informational), and it passed 116 rules with 0 failures (2 warnings).

Locust test report: a 20-second run with 97 requests, 0 failures, and response time percentiles per endpoint
ZAP scanning report for the backend: no High or Medium alerts, 2 Low, 5 informational, 27 endpoints

For the record, the ZAP baseline scan of the frontend, an unhardened Next.js dev server, passed 52 rules and warned on 15 (mostly missing security headers), with 0 failures. Treat these results as a demonstration of the tooling, not as a security audit.

Every report opens straight in a browser with no server needed, and is regenerated on each run. GitHub's CI runs only the static checks, such as Python and Dockerfile lint and the frontend type check. The tests above need the running stack, so they're run locally.

Summary

NASEBANAL Quickstarts isn't meant to report one particular experiment. It's an entry point for trying the building blocks that are specific to microservices, and the tools that verify them, in a sandbox on your own machine. Being able to run the container operations end to end with simple commands, and to see the interplay between services and tools in seven working scenarios, is what I think sets it apart. And because the results are left behind as test reports, you can look back at what worked and share it afterward.

We plan to keep adding modules and scenarios as the NASEBANAL Stack evolves, and to carry the test results gathered along the way into a mechanism for managing them in one place, which we'll build out as NASEBANAL Evolution.

The steps for each scenario are in the NASEBANAL Quickstarts README and in the docs inside the demo app that make apps:up starts. If you're curious, start with make apps:up.