MockyFast is a Python CLI tool for mocking HTTP APIs locally using YAML, JSON, and CSV-backed datasets.
It helps you simulate external services during local development without relying on hosted mock platforms or remote dashboards.
Because sometimes you just need a fast, local, and controllable way to simulate APIs while developing.
No external mock platforms, no unnecessary setup — just local files, a local server, and a workflow you control.
Getting started
- serve a folder of JSON/CSV files as a REST API with no configuration at all
- declare a whole CRUD resource in a few lines with
resources: - generate a config from existing data with
init --from-data - generate a config from an OpenAPI spec with
init --from-openapi - browse the served routes at
/ - restart automatically on config changes with
serve --reload
Responses
- inline bodies, external JSON files, and CSV/JSON-backed data sources
- templates:
{{uuid}},{{now}},{{randint:1:9}}, and echoes of the request - path parameters
- CSV type coercion and schema mapping
- response shaping:
wrap,not_found_status,not_found_body - answer differently on successive calls with
responses:
Behaviour
- stateful in-memory CRUD with
mutable, optionally saved across restarts - filtering, sorting and paging on list routes
- request matching by query params, headers and JSON body, with operators
(
matches,contains,one_of,gte,absent, …) - latency with
delay_ms, fixed or as a range - failure simulation with
fault
Tooling
validatechecks the config before the server starts, and warns about unreachable routesexplainshows which route answers a request, and why the others do notvalidate --against spec.yamlreports where the mock and a real spec disagreeschemapublishes a JSON Schema, for autocompletion and live validation in your editor- a real OpenAPI document at
/openapi.json, browsable at/docs, built from the config rather than from the handlers - permissive CORS by default, so a browser app can call the mock
- automated tests with
pytest
- Installation · Commands · Quick start
- Responses: inline · external files · data-driven · templates · sequences
- Resources: shorthand · stateful CRUD · filter/sort/page · persistence
- Behaviour: request matching · latency and faults · notes
- Tooling: validation · editor support · OpenAPI · explain · development
git clone https://github.com/Cartenone/MockyFast.git
cd MockyFast
pip install .pip install -e ".[dev]"mockyfast init
mockyfast validate mockyfast.yaml
mockyfast serve mockyfast.yaml --port 8000
mockyfast explain mockyfast.yaml GET /users/1
mockyfast schema > mockyfast.schema.json
mockyfast openapi mockyfast.yaml > openapi.jsonmkf init
mkf validate mockyfast.yaml
mkf serve mockyfast.yaml --port 8000mkf init [--output FILE] [--from-data PATH | --from-openapi FILE]
mkf schema [--output FILE]
mkf openapi CONFIG [--output FILE]
mkf validate CONFIG [--against SPEC]
mkf serve CONFIG [--host HOST] [--port PORT] [--reload] [--no-index] [--no-cors]
mkf explain CONFIG METHOD TARGET [-H/--header 'Name: value']... [--body JSON]
| Command | Option | Description |
|---|---|---|
init |
--output <file> |
Where to write the config (default mockyfast.yaml) |
init |
--from-data <path> |
Generate the config from a data file or folder |
init |
--from-openapi <file> |
Generate the config from an OpenAPI 3 document |
schema |
-o / --output <file> |
Write the JSON Schema to a file instead of standard output |
openapi |
-o / --output <file> |
Write the OpenAPI document to a file; .yaml writes YAML |
serve |
--host |
Bind address (default 127.0.0.1) |
serve |
--port |
Bind port (default 8000) |
serve |
--reload |
Restart when the config or its data files change |
serve |
--no-index |
Do not serve the generated route index at / (on by default) |
serve |
--no-cors |
Do not send permissive CORS headers (on by default) |
validate |
--against <file> |
Check what the mock answers against an OpenAPI 3 document |
explain |
-H / --header |
Request header as 'Name: value'; repeatable |
explain |
--body '{...}' |
JSON request body |
CONFIG is a YAML file, a data folder, or a single .json/.csv data file.
explain exits non-zero when no route answers the request.
Point MockyFast at a folder of .json or .csv files and it derives a full
CRUD API from them:
mkf serve ./datadata/
├─ users.json -> GET/POST /users, GET/PUT/PATCH/DELETE /users/{id}
└─ products.csv -> GET/POST /products, GET/PUT/PATCH/DELETE /products/{sku}
The key field is detected automatically: id when the data has one, otherwise
the first column. Open http://127.0.0.1:8000/ to see every route that was
generated.
When you outgrow it, write the equivalent config out and edit it by hand:
mkf init --from-data ./dataCreate a sample config:
mkf initValidate it:
mkf validate mockyfast.yamlStart the mock server:
mkf serve mockyfast.yaml --port 8000Then call it:
curl http://127.0.0.1:8000/healthroutes:
- method: GET
path: /health
response:
status_code: 200
body:
ok: trueA configuration can declare the format version it was written against:
version: 1
routes:
- method: GET
path: /health
response:
status_code: 200
body:
ok: trueIt is optional, and 1 is the only version MockyFast reads today. Writing it
down means a later change to the format can be introduced without breaking this
file: a version this build does not know is refused with a message instead of
being misread.
routes:
- method: GET
path: /users
response:
status_code: 200
body_from: ./responses/users.json{
"users": [
{ "id": 1, "name": "Mario" },
{ "id": 2, "name": "Luigi" }
]
}MockyFast can build responses from local CSV or JSON files, making mocks more dynamic and reusable.
A data source is declared under response.data_source:
| Key | Required | Description |
|---|---|---|
type |
yes | csv or json |
file |
yes | Path relative to the config file |
mode |
for reads | all returns a list, first returns a single object |
where |
no | Filter rows by a path or query parameter |
wrap |
no | Wrap the result under a key |
not_found_status |
no | Status used when mode: first finds nothing (default 404) |
not_found_body |
no | Body used when mode: first finds nothing |
coerce_types |
no | CSV only — infer primitive types |
schema |
no | CSV only — explicit type mapping |
list_query |
no | Allow filtering, sorting and paging on a mode: all route |
mutable |
no | Serve the file from a writable in-memory store |
persist |
no | Keep writes across restarts (true, or a path) |
key_field |
with mutable |
Primary key of the resource |
resource_name |
with mutable |
Store identity shared across routes |
routes:
- method: GET
path: /users
response:
data_source:
type: csv
file: ./data/users.csv
mode: all
wrap: items
- method: GET
path: /users/{user_id}
response:
data_source:
type: csv
file: ./data/users.csv
mode: first
where:
column: id
equals_path_param: user_id
not_found_status: 404
not_found_body:
error: user_not_foundid,name,active,balance
1,Mario,true,12.5
2,Luigi,false,7curl http://127.0.0.1:8000/users
curl http://127.0.0.1:8000/users/1
curl http://127.0.0.1:8000/users/999{
"items": [
{
"id": "1",
"name": "Mario",
"active": "true",
"balance": "12.5"
},
{
"id": "2",
"name": "Luigi",
"active": "false",
"balance": "7"
}
]
}A JSON data source works the same way, but the file must contain a root list of objects and the filter key is field instead of column.
routes:
- method: GET
path: /users/{user_id}
response:
data_source:
type: json
file: ./data/users.json
mode: first
where:
field: id
equals_path_param: user_id[
{ "id": 1, "name": "Mario", "role": "admin" },
{ "id": 2, "name": "Luigi", "role": "user" },
{ "id": 3, "name": "Anna", "role": "user" }
]JSON values keep their original types, so coerce_types and schema are not needed (and not supported) for JSON sources.
where can read a query parameter instead of a path parameter:
where:
field: role
equals_query_param: rolecurl "http://127.0.0.1:8000/users?role=admin"Exactly one of equals_path_param or equals_query_param must be set.
You can automatically coerce CSV values into Python/JSON primitive types.
routes:
- method: GET
path: /users/{user_id}
response:
data_source:
type: csv
file: ./data/users.csv
mode: first
where:
column: id
equals_path_param: user_id
coerce_types: trueWith coerce_types: true, values such as:
true→truefalse→false12→1212.5→12.5
are returned as properly typed JSON values.
For more control, you can define an explicit schema:
routes:
- method: GET
path: /users/{user_id}
response:
data_source:
type: csv
file: ./data/users.csv
mode: first
where:
column: id
equals_path_param: user_id
schema:
id: int
active: bool
balance: floatSupported schema types:
strintfloatbool
When schema is present, it takes precedence over coerce_types.
Declaring the five routes of a REST resource by hand means repeating the same
data_source block five times. A resources: entry describes the resource
once and expands into the equivalent routes before validation runs:
resources:
- name: users
path: /users
source:
type: json
file: ./data/users.json
key_field: id
wrap: items
not_found_body:
error: user_not_foundThat produces six routes:
GET /users
GET /users/{id}
POST /users
PUT /users/{id}
PATCH /users/{id}
DELETE /users/{id}
| Key | Required | Default | Description |
|---|---|---|---|
name |
yes | — | Resource name, also the store identity |
path |
no | /<name> |
Base path of the collection |
source |
yes | — | type (csv/json) and file |
key_field |
no | id |
Primary key, also the path parameter name |
methods |
no | all | Any of list, get, create, update, delete |
wrap |
no | — | Applied to the list route only |
not_found_status |
no | 404 |
Applied to the single-resource routes |
not_found_body |
no | — | Applied to the single-resource routes |
delay_ms |
no | — | Applied to every generated route |
list_query |
no | true |
Filtering, sorting and paging on the list route |
persist |
no | — | Keep writes across restarts (true, or a path) |
Resources are always stateful: they expand into mutable data sources sharing
one store, so a POST is visible to every other route of the resource.
Read-only resources are just a restricted method list:
resources:
- name: countries
source:
type: csv
file: ./data/countries.csv
methods: [list, get]routes: and resources: can live in the same file. Declared routes are
registered first, so a hand-written /users/me still wins over the generated
/users/{id}:
routes:
- method: GET
path: /users/me
response:
body:
id: 1
name: Mario
resources:
- name: users
source:
type: json
file: ./data/users.jsonSet mutable: true to turn a data source into a writable in-memory resource. The file is read once at startup to seed the store, and every request after that reads and writes the in-memory copy.
The data file on disk is never modified. Restarting the server resets the resource to its seeded state.
Two keys are required alongside mutable:
key_field— the primary key of the resourceresource_name— the store identity. Every route that should share the same data must use the sameresource_name, because/usersand/users/{user_id}are different paths and would otherwise be separate stores.
routes:
- method: GET
path: /users
response:
data_source:
type: json
file: ./data/users.json
mode: all
mutable: true
key_field: id
resource_name: users
wrap: items
- method: POST
path: /users
response:
status_code: 201
data_source:
type: json
file: ./data/users.json
mutable: true
key_field: id
resource_name: users
- method: GET
path: /users/{user_id}
response:
data_source:
type: json
file: ./data/users.json
mode: first
mutable: true
key_field: id
resource_name: users
where:
field: id
equals_path_param: user_id
- method: PUT
path: /users/{user_id}
response:
data_source:
type: json
file: ./data/users.json
mutable: true
key_field: id
resource_name: users
where:
field: id
equals_path_param: user_id
- method: DELETE
path: /users/{user_id}
response:
data_source:
type: json
file: ./data/users.json
mutable: true
key_field: id
resource_name: users
where:
field: id
equals_path_param: user_idcurl -X POST http://127.0.0.1:8000/users \
-H 'content-type: application/json' \
-d '{"id": 4, "name": "Giulia"}'
curl http://127.0.0.1:8000/users/4
curl -X PUT http://127.0.0.1:8000/users/4 \
-H 'content-type: application/json' \
-d '{"name": "Giulia Updated"}'
curl -X DELETE http://127.0.0.1:8000/users/4| Method | mode |
where |
Behaviour |
|---|---|---|---|
GET |
required | optional | Reads from the store, honouring wrap and not_found_* |
POST |
ignored | not used | Creates a resource from the request body |
PUT |
ignored | required | Replaces the resource with the request body |
PATCH |
ignored | required | Merges the request body into the resource |
DELETE |
ignored | required | Removes the matched resource, returns {"deleted": true} |
- The request body must be a JSON object — anything else returns
400. POSTrequireskey_fieldin the body (400if missing) and rejects an existing key with409.PUTreplaces the resource: fields absent from the body are dropped.PATCHmerges, preserving them. The key field survives both.PUTandPATCHcannot changekey_field— attempting to do so returns400.- When
PUT,PATCH, orDELETEmatch nothing, the configurednot_found_status/not_found_bodyare used (default404).
A mode: all route with list_query: true reads a handful of query
parameters. The resources: shorthand turns this on for the list route, so it
works out of the box in zero-config mode; an explicit route has to ask for it.
| Parameter | Meaning |
|---|---|
?field=value |
Keep rows whose field equals value |
_sort=field |
Sort ascending; _sort=a,b sorts by several fields |
_order=desc |
Reverse the sort |
_limit=10 |
Page size |
_page=2 |
Page number, 1-based, used together with _limit |
_offset=20 |
Skip rows, as an alternative to _page |
curl "http://127.0.0.1:8000/users?role=user&_sort=age&_limit=10&_page=2"Every response carries X-Total-Count with the number of rows before
paging, so a client can render a pager. Numbers sort before text, so a column
holding both still comes back in a stable order. An unusable value — a
non-numeric _limit, an unknown _sort field — is ignored rather than
rejected.
A query parameter already used by where is not treated as a field filter.
Opt out on a resource with list_query: false.
By default a mutable resource starts again from its data file on every run.
Add persist and writes are saved to a separate state file:
resources:
- name: users
source:
type: json
file: ./data/users.json
persist: truepersist: truewrites to.mockyfast-state/<name>.jsonnext to the config.persist: ./stato/utenti.jsonwrites wherever you say, as long as it stays inside the configuration directory.
The data file remains the seed and is never written to, so it stays
versionable and readable as documentation. Delete the state file to start over.
State files are written on every write and swapped into place atomically; add
.mockyfast-state/ to your .gitignore.
You can use path parameters in the route path and reference them in the response body.
routes:
- method: GET
path: /users/{user_id}
response:
status_code: 200
body:
id: "{user_id}"
name: "User {user_id}"Example:
curl http://127.0.0.1:8000/users/123Response:
{
"id": "123",
"name": "User 123"
}Besides {path_param}, response bodies support {{...}} placeholders that read
the request or generate a value. They work in body, in body_from files, in
nested structures, and in object keys.
| Placeholder | Result |
|---|---|
{{uuid}} |
A random UUID v4 |
{{now}} |
Current UTC time, ISO 8601 |
{{now:%Y-%m-%d}} |
Current UTC time, strftime format |
{{timestamp}} |
Current Unix time, as a number |
{{randint:1:100}} |
Random integer in range |
{{randfloat:0:9.99}} |
Random float in range, 2 decimals |
{{choice:gold|silver}} |
One of the options |
{{path.user_id}} |
Path parameter (same as {user_id}) |
{{query.page}} |
Query parameter |
{{header.x-client}} |
Request header, case-insensitive |
{{body.customer.email}} |
Request body, dotted path; list indexes work too |
routes:
- method: POST
path: /orders/{order_id}
response:
status_code: 201
body:
id: "{{uuid}}"
order: "{order_id}"
created_at: "{{now}}"
quantity: "{{randint:1:5}}"
confirmation: "Order {order_id} for {{body.customer.email}}"Two rules make the output predictable:
- A string that is exactly one placeholder keeps the placeholder's type.
"{{randint:1:5}}"yields the number3, not the string"3". Put the placeholder inside other text and you get a string. - An unknown or unresolvable placeholder is left as written. A typo shows up in the response instead of raising, which is easier to spot while iterating.
mockyfast can return different responses for the same path depending on the request.
Routes are evaluated in declaration order, and the first one whose matchers all pass wins. Put the most specific route first.
routes:
- method: GET
path: /orders
request:
query:
status: shipped
response:
status_code: 200
body:
items:
- id: 1
status: shipped
- method: GET
path: /orders
response:
status_code: 200
body:
items: []routes:
- method: GET
path: /profile
request:
headers:
Authorization: Bearer secret-token
response:
status_code: 200
body:
user: mario
- method: GET
path: /profile
response:
status_code: 401
body:
error: unauthorizedroutes:
- method: POST
path: /login
request:
json:
username: admin
password: secret
response:
status_code: 200
body:
token: fake-jwt-token
- method: POST
path: /login
response:
status_code: 401
body:
error: invalid_credentialsA matcher value can be an object of operators instead of a literal:
routes:
- method: POST
path: /signup
request:
headers:
Authorization: { matches: '^Bearer .{8,}$' }
query:
page: { one_of: ['1', '2'] }
json:
email: { matches: '@' }
age: { gte: 18 }
referral: { absent: true }
response:
status_code: 201
body:
ok: true| Operator | Meaning |
|---|---|
equals |
Exact value (the default for a plain scalar) |
matches |
Regular expression, searched anywhere in the value |
contains |
Substring, or membership for lists |
one_of |
Value is in the given list |
present: true |
Key exists, whatever its value |
absent: true |
Key must not be present |
gt gte lt lte |
Numeric comparison |
Several operators in one object must all pass. An object counts as a matcher
only when every key is an operator, so a nested body object such as
{"user": {"name": "Mario"}} stays a structural comparison.
mkf validate compiles every matches regex, so a broken pattern is caught
before the server starts.
Matching is partial for query and headers (extra values in the request are ignored) and for object keys in json. Lists inside json must match exactly, including length.
query and headers compare as text, since that is what HTTP carries: { equals: 2 } matches ?page=2. Inside json the comparison is typed, so { equals: 5 } matches the number 5 and not the string "5".
delay_ms takes a fixed number of milliseconds, or a range for latency that
varies from call to call:
routes:
- method: GET
path: /slow
response:
delay_ms: 3000
body:
ok: true
- method: GET
path: /jittery
response:
delay_ms:
min: 50
max: 800
body:
ok: trueThe delay applies to every response the route produces, including not-found and CRUD responses.
fault replaces the normal response some of the time, which is how you exercise
a client's retry and timeout handling:
routes:
- method: GET
path: /flaky
response:
body:
ok: true
fault:
probability: 0.2
status_code: 503
body:
error: overloaded| Key | Default | Meaning |
|---|---|---|
probability |
1 |
Chance the fault fires, from 0 to 1 |
status_code |
500 |
Status of the fault response |
body |
{"detail": "Injected fault"} |
Body of the fault response |
delay_ms |
route delay | Time the fault takes; use it to simulate a timeout |
Declaring fault: {} is enough to fail every call with the defaults.
Use responses: instead of response: to answer differently on successive
calls, which is what a polling client needs to be tested against:
routes:
- method: GET
path: /jobs/{job_id}
responses:
- status_code: 202
body:
status: accepted
- status_code: 202
body:
status: running
- status_code: 200
body:
status: donecurl http://127.0.0.1:8000/jobs/7 # 202 accepted
curl http://127.0.0.1:8000/jobs/7 # 202 running
curl http://127.0.0.1:8000/jobs/7 # 200 done
curl http://127.0.0.1:8000/jobs/7 # 200 done, the last entry repeatsEach entry is a full response object: status_code, body, body_from,
delay_ms, fault and templates all work inside one. The position is counted
per route, not per path parameter, and resets when the server restarts.
The server publishes an OpenAPI document at /openapi.json, and a Swagger page
at /docs that reads it. Both are built from the configuration rather than from
the handlers: every route is served by the same generic function, so a document
derived from the code would describe none of them.
curl http://127.0.0.1:8000/openapi.jsonWrite it out without starting a server:
mkf openapi mockyfast.yaml > openapi.json
mkf openapi mockyfast.yaml -o openapi.yamlThe .yaml suffix decides the format.
| From the config | In the document |
|---|---|
| the rows of a data source | the response schema, with an example |
mode: all / mode: first |
an array, or a single object |
wrap |
the key the result sits under |
status_code, and each entry of a responses: sequence |
one response each |
not_found_status / not_found_body |
the not-found response |
fault |
the fault status and its body |
| a mutable resource | 400 and 409 on writes, and the body a write expects |
list_query |
_limit, _page, _offset, _sort, _order, X-Total-Count |
request.query / request.headers |
query and header parameters |
request.json |
the request body schema |
where |
which field the path or query parameter selects |
| several routes on one path | one operation, described in matching order |
Types are read off the data, so a CSV column stays a string until coerce_types
or schema says otherwise, and "{{randint:1:5}}" is documented as an integer
because that is what it renders to. A matching operator such as { gte: 18 }
constrains a value without naming its type, so it is left unconstrained.
Nothing in the document is a contract the server enforces: it describes what the mock answers, and it is inferred, so a data file it cannot read costs a schema rather than a running server.
The document is built when the server starts. Editing a non-mutable data file
changes what the routes answer immediately, but the document catches up only on
the next restart; serve --reload restarts for you.
The other direction: mkf init --from-openapi reads an OpenAPI 3 document and
writes the routes that answer it.
mkf init --from-openapi ./openapi.yaml
mkf serve mockyfast.yamlEach operation becomes one route, answering the lowest success status the spec declares. Response schemas map onto the templates MockyFast already renders, so the mock returns plausible data instead of empty objects:
| In the spec | In the config |
|---|---|
an example, anywhere |
used as written, in preference to anything generated |
type: integer, with minimum / maximum |
{{randint:min:max}} |
type: number |
{{randfloat:min:max}} |
an enum of strings |
{{choice:a|b}} |
format: uuid, date-time, date |
{{uuid}}, {{now}}, {{now:%Y-%m-%d}} |
format: email, uri, ipv4, … |
a plausible constant |
type: array |
a list of two items |
$ref, allOf, oneOf |
resolved, merged, and first-of respectively |
An enum of numbers keeps its first value instead of becoming a {{choice:...}},
because that template hands back the text of the option it picked and the type
would be lost.
The result is a starting point rather than a translation: a spec says what an API may return, a mock says what it does return. Edit it.
The document has to be OpenAPI 3 and self-contained. A Swagger 2.0 file, or a
$ref pointing into another file, is refused with a message rather than
half-imported.
A mock drifts quietly. A field gets renamed in the real API, a status code is added, and the mock keeps answering what it always did, so the tests written against it keep passing while the client breaks.
mkf validate mockyfast.yaml --against ./openapi.yamlConfiguration is valid.
Not conformant: Route #1 GET /users: field '[].fullName' is not declared in the spec. The spec declares 'name'.
Not conformant: Route #1 GET /users: field '[].name' is required by the spec, and the response does not carry it.
Not conformant: Route #3 GET /users/{user_id}: the spec declares no status 418 for it, only 200, 404.
Checked 6 route(s) against the spec.
The spec declares 12 operation(s) that no route answers.
What is compared is the response a route really produces - templates rendered, rows read from the data file - not the configuration that describes it. The command exits non-zero when anything is not conformant, so it belongs in CI next to the tests.
It reports:
- a route answering a path, or a method, the spec does not declare
- a status code the operation does not declare,
2XXwildcards anddefaultincluded, countingnot_found_statusand an injectedfaulttoo - a response field the spec does not declare, naming the closest one it does, which is what a rename looks like from this side
- a field the spec requires that the response does not carry
- a field whose type is not the declared one
A path parameter may be named differently on each side: /users/{user_id} and
/users/{id} are the same operation.
Two things are deliberately left alone. A placeholder that could not be
rendered, such as {user_id}, says nothing about its type. A oneOf or an
anyOf says the value may take several shapes, and reporting it against the
first would be guessing.
The last line counts the operations the spec declares that no route answers. That is a note, not a failure: mocking part of an API is a normal thing to do.
When a request does not reach the route you expected, mkf explain walks the
same decisions the server makes:
mkf explain mockyfast.yaml GET /users/meGET /users/me
route #1 GET /users - path does not match
-> route #2 GET /users/{user_id} - matches
~ route #3 GET /users/me - would match, but an earlier route answers first
Answered by route #2: inline body
Path parameters: user_id=me
-> marks the winner, ~ marks a route that would match but is unreachable.
Headers and a body can be supplied so matchers are evaluated too:
mkf explain mockyfast.yaml POST /login --body '{"username":"admin","password":"wrong"}'
mkf explain mockyfast.yaml GET '/orders?status=shipped' -H 'Authorization: Bearer abc'The command exits non-zero when no route answers, so it can be used as a check.
- Route order matters.
/users/medeclared after/users/{user_id}is never reached. Declare static paths first. - Non-mutable data files are re-read on every request. Editing a CSV or JSON data source is picked up without restarting the server.
mutablesources are the exception: they are read once at startup. - Referenced files must stay inside the config directory.
body_fromanddata_source.filecannot escape the folder containing the YAML file. - State is per-process. The in-memory store is not shared between server restarts or between multiple processes.
- CORS is permissive by default (
Access-Control-Allow-Origin: *, without credentials), because a mock server exists to be called from a dev server on another port. Turn it off withserve --no-cors. - Resource names must be unique. The name identifies the shared store, so two resources claiming one name is an error rather than a silent merge. In zero-config mode this means
users.jsonandusers.csvcannot sit in the same folder. - The route index reports file names, not paths, so it does not publish your directory layout.
--reloadneedswatchfiles, which ships as a dependency. Without it uvicorn falls back to a reloader that only watches*.py, so config changes would go unnoticed;serve --reloadrefuses to start rather than pretend.- The index at
/is generated only when no route claims that path. Declare your ownGET /and it takes over.
Imagine your application depends on an external service.
Instead of calling the real service during local development, you can point your app to http://127.0.0.1:8000 and let mockyfast simulate the API.
That makes it easier to:
- develop locally
- reproduce edge cases
- test success and error responses
- work without depending on external environments
mocks/
├─ mockyfast.yaml
├─ data/ seed data, versioned
│ ├─ users.csv
│ └─ users.json
├─ responses/ whole bodies for body_from
│ └─ users.json
└─ .mockyfast-state/ written by `persist`, gitignored
└─ users.json
Then run:
mkf serve ./mocks/mockyfast.yaml --port 8000Before starting the server, you can validate your configuration:
mkf validate mockyfast.yamlThis helps catch issues like:
- missing
routesorresources - invalid route structure
- unknown HTTP methods, or paths not starting with
/ - missing JSON or CSV files
- files referenced outside the configuration directory
- invalid
status_code,delay_msordelay_msrange - invalid request matching config, including regular expressions that do not compile and operator arguments of the wrong type
- invalid CSV schema configuration
- incomplete
mutableconfiguration (key_field,resource_name,where) - invalid
resources:entries, and duplicate resource names - a route defining both
responseandresponses, or an emptyresponses - invalid
faultsettings persistwithoutmutable- unknown keys, which are almost always typos
- a key written but left empty, where a value is required
- an unsupported
version
It also reports warnings that do not make a config invalid, such as a route made unreachable by an earlier, more general one.
With --against, it goes on to compare what the mock answers with an OpenAPI
document; see checking a mock against the spec.
mkf schema prints a JSON Schema generated from the same models mkf validate
runs, so your editor and the CLI cannot disagree about what a configuration may
contain:
mkf schema > mockyfast.schema.jsonThe generated file is also published in this repository, at
mockyfast.schema.json, so you can point at it
without generating anything.
Put a modeline at the top of the configuration. The YAML extension for VS Code reads it, as does any editor speaking the YAML language server protocol:
# yaml-language-server: $schema=https://raw.githubusercontent.com/Cartenone/MockyFast/main/mockyfast.schema.json
version: 1
routes:
- method: GET
path: /health
response:
status_code: 200
body:
ok: trueIn .vscode/settings.json:
{
"yaml.schemas": {
"https://raw.githubusercontent.com/Cartenone/MockyFast/main/mockyfast.schema.json": [
"mockyfast.yaml",
"mocks/**/*.yaml"
]
}
}Either form accepts a local file too: replace the URL with
./mockyfast.schema.json and the editor validates against the schema of the
version you have installed.
Run the test suite:
pytestWith coverage:
pytest --cov=mockyfast --cov-report=term-missingLint:
ruff check .Planned improvements:
- an admin API to reset state and inspect received requests
- richer body matching (JSONPath)
- faker-style generators for names, emails and addresses
- capture real API responses into reusable mock files
Future exploration:
- record & replay proxy mode
- GraphQL support
- WebSocket mocking
- gRPC support
- SOAP/XML support
Created by Cartenone.
MIT
