blendwerk

[ˈblɛntvɛrk] — German: illusion, deceptive appearance.

A file-based mock HTTP/HTTPS server where your directory structure IS the API. No databases, no config files — just directories and text files.

Directory = API

Your directory structure IS your API. Folders become URL paths, filenames become HTTP methods. No config files, no databases.

Ready in Seconds

Create a file, run blendwerk, done. Hot-reload lets you incrementally build your mocks as you develop and test.

SSL Out of the Box

Self-signed certificates generated automatically. Test your HTTPS clients without certificate hassle. Custom certs supported too.

Terminal

Quick Start

cargo install blendwerk

Available on crates.io

Download pre-built binaries from GitHub Releases:

View Releases
git clone https://github.com/jakobwesthoff/blendwerk.git
cd blendwerk
cargo build --release

Basic Usage

# Create a mock
mkdir -p mocks/api/users
cat > mocks/api/users/GET.json << 'EOF'
---
status: 200
---
{"users": [{"id": 1, "name": "Alice"}]}
EOF

# Run
blendwerk ./mocks

# Test
curl http://localhost:8080/api/users

Documentation

Point blendwerk at a directory and it serves mock HTTP/HTTPS responses based on the file structure. No configuration needed — just files and folders.

blendwerk <DIRECTORY> [OPTIONS]

Core Concepts

Directory Structure Maps to Routes

Your directory structure IS your API. Folders become URL paths, filenames become HTTP methods. That's it.

mocks/
├── api/
   ├── users/
   ├── GET.json          # GET /api/users
   ├── POST.json         # POST /api/users
   └── [id]/             # Path parameter
       ├── GET.json      # GET /api/users/:id
       ├── PUT.json      # PUT /api/users/:id
       └── DELETE.json   # DELETE /api/users/:id
   └── health/
       └── GET.json          # GET /api/health
└── GET.html                  # GET /

Rules:

  • Method names are case-insensitive (GET.json, get.json, Get.json all work)
  • Use [paramName] directories for path parameters (matches any path segment)
  • Hot-reload: changes to files are detected automatically

Route Matching: Routes use first-match-wins ordering. Both static routes and [param] routes are matched in discovery order.

Error Responses:

  • 404 Not Found — No route matches the path
  • 405 Method Not Allowed — Path exists but method isn't defined

Query Parameters: Query strings don't affect route matching — all requests to a path use the same mock regardless of query parameters. However, query parameters are captured in request logs.

Response Files

Format

Response files use optional YAML frontmatter followed by the response body:

---
status: 201
headers:
  X-Request-Id: abc-123
  Cache-Control: no-cache
delay: 100
---
{"created": true, "id": 42}

Frontmatter Fields

FieldTypeDefaultDescription
statusinteger200HTTP status code
headersmap{}Response headers
delayinteger0Delay in milliseconds before responding

All fields are optional. Files without frontmatter return status 200.

Content-Type

Automatically inferred from file extension (can of course be overridden in headers):

  • .jsonapplication/json
  • .htmltext/html
  • .xmlapplication/xml
  • .txttext/plain

Examples

Error response:

# mocks/api/protected/GET.json
---
status: 401
headers:
  WWW-Authenticate: Bearer realm="api"
---
{"error": "unauthorized"}

Simulating latency:

# mocks/api/slow/GET.json
---
delay: 2000
---
{"message": "This took 2 seconds"}

Multiple methods:

# mocks/api/items/GET.json
{"items": []}

# mocks/api/items/POST.json
---
status: 201
---
{"created": true}

Configuration

Command Line Options

Usage: blendwerk [OPTIONS] <DIRECTORY>

Arguments:
  <DIRECTORY>
          Directory containing mock responses

Options:
  -p, --http-port <HTTP_PORT>
          HTTP port
          [default: 8080]

  -s, --https-port <HTTPS_PORT>
          HTTPS port
          [default: 8443]

      --http-only
          Only serve HTTP (no HTTPS)

      --https-only
          Only serve HTTPS (no HTTP)

      --cert-mode <CERT_MODE>
          Certificate mode

          Possible values:
          - none:        No HTTPS, HTTP only
          - self-signed: Generate self-signed certificate on startup
          - custom:      Use custom certificate files

          [default: self-signed]

      --cert-file <CERT_FILE>
          Path to certificate file (required for custom cert mode)

      --key-file <KEY_FILE>
          Path to private key file (required for custom cert mode)

      --request-log <REQUEST_LOG>
          Directory to log all incoming requests

      --request-log-format <REQUEST_LOG_FORMAT>
          Format for request logs

          [default: json]
          [possible values: json, yaml]

  -h, --help
          Print help

  -V, --version
          Print version

HTTP/HTTPS Modes

Default (HTTP + HTTPS with self-signed cert):

blendwerk ./mocks
# HTTP on :8080, HTTPS on :8443

HTTP only:

blendwerk ./mocks --http-only
# or
blendwerk ./mocks --cert-mode none

HTTPS only:

blendwerk ./mocks --https-only

Custom certificate:

blendwerk ./mocks --cert-mode custom --cert-file server.crt --key-file server.key

Request Logging

blendwerk can log all incoming requests to a directory structure that mirrors your API routes. This is useful for debugging, testing, and understanding how your mock API is being used.

Enable request logging:

blendwerk ./mocks --request-log ./request-logs

Directory structure:

request-logs/
├── api/
   └── users/
       ├── GET/
   ├── 2025-01-28T15-30-45.123456Z_01HQKP6J9Z0000000000000000.json
   └── 2025-01-28T15-31-12.456789Z_01HQKP7A1A0000000000000000.json
       └── POST/
           └── 2025-01-28T15-32-00.789012Z_01HQKP8B2B0000000000000000.json

Log file format:

Each request is logged as a separate file containing complete request and response information:

{
  "metadata": {
    "timestamp": "2025-01-28T15-30-45.123456Z",
    "request_id": "01HQKP6J9Z0000000000000000"
  },
  "request": {
    "method": "GET",
    "uri": "/api/users?page=2",
    "path": "/api/users",
    "query": "page=2",
    "headers": {
      "user-agent": "curl/8.0.0",
      "accept": "*/*"
    },
    "body": null,
    "matched_route": "/api/users"
  },
  "response": {
    "status": 200,
    "headers": {
      "content-type": "application/json"
    },
    "body": "{\"users\": [...]}",
    "delay_ms": 0
  }
}

YAML format:

blendwerk ./mocks --request-log ./request-logs --request-log-format yaml

Filenames use ISO 8601 timestamps plus ULIDs for sortability and uniqueness. Logging happens asynchronously and doesn't block responses. 404s are logged to their requested paths (e.g., a request to /api/nonexistent creates a log file in request-logs/api/nonexistent/GET/).

Route Matching

When multiple routes could match a request, blendwerk uses first-match-wins ordering. Routes are matched in the order they're discovered during directory scanning.

Static vs Dynamic Routes

Static routes (exact paths) and dynamic routes (with [param] segments) are treated equally — the first match wins. If you need a specific path to take precedence:

mocks/api/users/
├── admin/
   └── GET.json      # GET /api/users/admin (static)
└── [id]/
    └── GET.json      # GET /api/users/:id (dynamic)

Both routes exist, and requests to /api/users/admin will match the static route if it's discovered first.

Multiple Path Parameters

You can use multiple [param] segments for nested resources:

mocks/api/users/[userId]/posts/[postId]/
├── GET.json          # GET /api/users/:userId/posts/:postId
├── PUT.json          # PUT /api/users/:userId/posts/:postId
└── DELETE.json       # DELETE /api/users/:userId/posts/:postId

Query Parameters

Query strings do not affect route matching — all requests to a path use the same mock response regardless of query parameters:

# All these hit the same mock: mocks/api/users/GET.json
curl http://localhost:8080/api/users
curl http://localhost:8080/api/users?page=1
curl http://localhost:8080/api/users?page=2&limit=10

However, query parameters are logged when request logging is enabled, so you can see exactly what your application is requesting.

Request Logging Details

Understanding the logged fields:

FieldDescription
pathThe literal request path (e.g., /api/users/42)
matched_routeThe route pattern that matched (e.g., /api/users/:id)
queryQuery string if present, otherwise null

404 requests are also logged to their requested paths. A request to /nonexistent/path creates a log file at request-logs/nonexistent/path/GET/...

Cookbook

RESTful CRUD API:

mocks/api/users/
├── GET.json                  # List users
├── POST.json                 # Create user (status: 201)
└── [id]/
    ├── GET.json              # Get single user
    ├── PUT.json              # Update user
    └── DELETE.json           # Delete user (status: 204, empty body)

Error responses with custom headers:

# mocks/api/admin/GET.json
---
status: 403
headers:
  X-Error-Code: FORBIDDEN
  X-Error-Message: Admin access required
---
{"error": "forbidden", "message": "Admin access required"}

CORS preflight response:

# mocks/api/data/OPTIONS.json
---
status: 204
headers:
  Access-Control-Allow-Origin: "*"
  Access-Control-Allow-Methods: GET, POST, PUT, DELETE
  Access-Control-Allow-Headers: Content-Type, Authorization
---

Simulating slow API (rate limiting test):

# mocks/api/heavy-operation/POST.json
---
delay: 3000
status: 202
---
{"status": "processing", "estimatedTime": "3 seconds"}

Override Content-Type:

# mocks/api/legacy/GET.json - serve JSON with custom content type
---
headers:
  Content-Type: application/vnd.api+json
---
{"data": {"type": "users", "id": "1"}}

Docker Container Support

blendwerk properly handles running as PID 1, so you can run it directly in containers without worrying about zombie processes or signal handling. When running as PID 1 (the init process), it automatically:

  • Reaps zombie processes
  • Handles SIGTERM and SIGINT for graceful shutdown
  • Forwards signals to child processes

This behavior is autodetected and requires no configuration:

FROM scratch
COPY blendwerk /blendwerk
COPY mocks /mocks
ENTRYPOINT ["/blendwerk"]
CMD ["/mocks"]

Limitations

Memory Usage: blendwerk loads all mock response files into memory at startup (and on hot-reload). This keeps things blazing fast for development and testing, but means you probably shouldn't throw gigabyte-sized video files or massive datasets at it. If you're mocking endpoints that return large binary chunks, keep an eye on your RAM.

Production Use: Look, I think blendwerk is pretty cool, and it's great for local development, integration testing, and temporary mock services. But it's not nginx. It's not built to be a battle-hardened production web server handling millions of requests. If you find yourself thinking "maybe I should use this in production for real traffic"... maybe take a step back and consider if you're solving the right problem. That said, for what it's designed to do - providing quick, file-based API mocks - it does it well.

Text Files Only: Response files are read as UTF-8 text. Binary responses (images, PDFs) are not supported.

Static Responses: Responses are static — you cannot vary the response based on request body, headers, or query parameters. Each (method, path) combination always returns the same response.