Skip to content

Stateful Mocking

Stateful mocking allows mockd to simulate real CRUD APIs where resources persist across requests. Create, update, and delete operations modify state that subsequent requests can observe.

Traditional mocks return static responses. Stateful mocking maintains an in-memory store that:

  • POST creates new resources
  • GET retrieves current resources
  • PUT replaces existing resources
  • DELETE removes resources

Changes persist for the lifetime of the server session.

Enable stateful mocking in your configuration:

{
"statefulResources": [
{
"name": "users",
"basePath": "/api/users",
"idField": "id"
}
]
}

Start the server and interact:

Terminal window
# Create a user
curl -X POST http://localhost:4280/api/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'
# Response: {"id": 1, "name": "Alice", "email": "alice@example.com"}
# List users - Alice is now in the list
curl http://localhost:4280/api/users
# Response: [{"id": 1, "name": "Alice", "email": "alice@example.com"}]
# Get single user
curl http://localhost:4280/api/users/1
# Response: {"id": 1, "name": "Alice", "email": "alice@example.com"}
# Update user
curl -X PUT http://localhost:4280/api/users/1 \
-H "Content-Type: application/json" \
-d '{"name": "Alice Smith", "email": "alice@example.com"}'
# Response: {"id": 1, "name": "Alice Smith", "email": "alice@example.com"}
# Delete user
curl -X DELETE http://localhost:4280/api/users/1
# Response: 204 No Content
# User is gone
curl http://localhost:4280/api/users/1
# Response: 404 Not Found
{
"statefulResources": [
{
"name": "users",
"basePath": "/api/users"
}
]
}
FieldDescriptionDefault
nameUnique resource nameRequired
basePathURL path prefix for the resourceRequired
idFieldField name for resource ID"id"
parentFieldParent FK field for nested resources-
seedDataInitial data array[]
{
"statefulResources": [
{
"name": "users",
"basePath": "/api/users"
},
{
"name": "posts",
"basePath": "/api/posts"
},
{
"name": "comments",
"basePath": "/api/posts/:postId/comments",
"parentField": "postId"
}
]
}

Pre-populate resources:

{
"statefulResources": [
{
"name": "users",
"basePath": "/api/users",
"seedData": [
{"id": "1", "name": "Alice", "email": "alice@example.com"},
{"id": "2", "name": "Bob", "email": "bob@example.com"}
]
}
]
}
Terminal window
POST /api/users
Content-Type: application/json
{"name": "Charlie", "email": "charlie@example.com"}

Response:

{
"id": 3,
"name": "Charlie",
"email": "charlie@example.com"
}

Status: 201 Created

Terminal window
GET /api/users

Response:

[
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
{"id": 3, "name": "Charlie"}
]
Terminal window
GET /api/users/2

Response:

{"id": 2, "name": "Bob", "email": "bob@example.com"}

Not found:

Terminal window
GET /api/users/999

Response: 404 Not Found

Replace entire resource:

Terminal window
PUT /api/users/2
Content-Type: application/json
{"name": "Robert", "email": "robert@example.com"}

Response:

{"id": 2, "name": "Robert", "email": "robert@example.com"}
Terminal window
DELETE /api/users/2

Response: 204 No Content

Handle parent-child relationships:

{
"statefulResources": [
{
"name": "posts",
"basePath": "/api/posts"
},
{
"name": "comments",
"basePath": "/api/posts/:postId/comments",
"parentField": "postId"
}
]
}

Comments are scoped to their parent post:

Terminal window
# Get comments for post 1
GET /api/posts/1/comments
# Create comment on post 1
POST /api/posts/1/comments
{"text": "Great post!"}

Filter by any field using query parameters:

Terminal window
GET /api/users?name=Alice
GET /api/users?status=active

Filtering is always enabled.

Use offset-based pagination:

Terminal window
GET /api/users?limit=10&offset=20
GET /api/users?sort=name&order=asc

Query parameters:

ParameterDescriptionDefault
limitMaximum items to return100
offsetItems to skip0
sortField to sort bycreatedAt
orderSort direction: “asc” or “desc”desc

Response includes pagination metadata:

{
"data": [...],
"meta": {
"total": 45,
"limit": 10,
"offset": 20,
"count": 10
}
}

Validate incoming requests before creating or updating resources. Validation ensures data integrity by checking field types, formats, constraints, and required fields.

statefulResources:
- name: users
basePath: /api/users
validation:
mode: strict
fields:
email:
type: string
required: true
format: email
username:
type: string
required: true
minLength: 3
maxLength: 30
pattern: "^[a-z][a-z0-9_]*$"
age:
type: integer
min: 0
max: 150
role:
type: string
enum: [admin, user, guest]
ModeBehavior
strictReject request on any validation failure (default)
warnLog warnings but allow request through
permissiveOnly fail on critical errors (missing required fields)

Validate nested object fields using dot notation:

fields:
"address.city":
type: string
required: true
"items.sku":
type: string
pattern: "^SKU-[A-Z0-9]+$"

For nested objects, array validation, formats, patterns, and more, see the Validation Guide.

State exists only in memory and resets when the server stops. Use seed data to pre-populate resources on startup.

Manage state via the admin API:

Terminal window
# Get state overview
GET /state
# Reset all state to seed data
POST /state/reset
# List all resources
GET /state/resources
# Get specific resource info
GET /state/resources/users
# Clear specific resource (remove all items)
DELETE /state/resources/users

Stateful resources work alongside traditional mocks:

{
"mocks": [
{
"matcher": {"method": "GET", "path": "/api/health"},
"response": {"statusCode": 200, "body": {"status": "ok"}}
}
],
"statefulResources": [
{
"name": "users",
"basePath": "/api/users"
}
]
}

Static mocks take priority when matched.

{
"server": {
"port": 4280
},
"statefulResources": [
{
"name": "users",
"basePath": "/api/users",
"idField": "id",
"seedData": [
{"id": "1", "name": "Admin", "role": "admin"}
]
},
{
"name": "posts",
"basePath": "/api/posts"
}
]
}