dwgx@blog:~$dwgx
> cd ../posts

imnot: a stateful mock server from a single YAML file

// created

imnot is a stateful API mock server: describe an external API in one YAML file, run imnot start, and you get a working mock. Adding endpoints needs no code changes. It’s written in Python on top of FastAPI, keeps session state in SQLite, and stays lightweight. I didn’t write it. The original author is edu2105; I forked it to hack on and study.

What it solves is the pain of external APIs in integration tests: third-party payments, OAuth token exchange, async submit-then-poll flows, webhook callbacks, all of them full of traps. You can’t hit a vendor’s sandbox for real in CI, it’s slow, flaky, and rate-limited. The old options were to write piles of mock code or stand up a heavy GUI. imnot handles it with a YAML declaration.

The most interesting part is that it builds “stateful” into the YAML rather than scattering it across scripts. A few patterns:

  • oauth client-credentials token exchange, returning a static JWT
  • static always spits back the hardcoded JSON written in the YAML
  • fetch synchronous GET that returns a previously stored payload
  • async an N-step async flow, from submit to status check to result retrieval, with the step count and methods configured in YAML
  • push returns immediately, then actively sends a payload to a callback URL, simulating the other side calling your webhook

Payloads split into two layers, global and session. Running tests concurrently relies on an X-Imnot-Session header for isolation: each test gets its own copy and they don’t step on each other, cleaner than everyone sharing one fixture. It also ships a resident admin API that can upload payloads and hot-reload the YAML (POST /imnot/admin/reload). POST a YAML fragment and a new partner route is mounted, no restart needed, and it can export a Postman collection too. One process in a container running long-term fits my self-hosting appetite nicely.

The fork is mainly to see how the handlers in imnot/engine/patterns/ turn YAML into dynamically registered FastAPI routes; the factory approach in router.py is what I most want to learn from. It doesn’t hide its weak spots either: push callbacks have no retry, there’s no native HTTPS so you need a reverse proxy to terminate TLS, there’s no web UI (JSON only), SQLite doesn’t span instances, and it’s a single-node toy. Plenty for running tests on a home machine.

One trap: partners written in over HTTP at runtime land on the container’s local filesystem, and they vanish the moment the Pod restarts. You need to mount persistent volumes at /app/partners and /app/data yourself. This isn’t a bug; it plainly states that persistence is your responsibility.