Skip to content
rustrumPublic

About

API prototyping/mocking server & rust unit tests library to mimic external 3rd party API endpoints with Rhai scripting capabilities

Topics

Resources

Stars

51 stars

Watchers

0 watching

Forks

Repository files navigation

Apate API mocking server

Crates.io Released API docs

Follow me on X(twitter) Threads Follow me on reddit

An API prototyping and mocking server whose main purpose is to help with integration and end-to-end testing. The project is named after Apate - the goddess and personification of deceit.

πŸš€ Project is stable. Almost everything works as it was planned. I will wait some time for user feedback. No breaking changes are expected in the near future.

Project is following zer0ver versioning for now. So there are no plans for 1.x.x release at all.

Features

  • πŸ’»βš™οΈ Standalone server app with a web UI
  • πŸ”ƒ Live spec reloading via UI or API
  • 🎭 Mocking any string & binary responses
  • ⛩️ Jinja templates to customize response bodies
  • 🌿 Rhai scripting for advanced scenarios
  • πŸ’Ύ In-memory persistence to mimic DB behavior in some cases
  • πŸ› οΈ Unit-test friendly Rust library
  • πŸ¦€ Ability to build a custom mocking server with your Rust extensions

Why do you need it ❔

  • πŸ‘¨πŸ»β€πŸ’» local development - so you don't have to run/build other services locally or call external APIs
  • πŸ¦€ Rust unit tests - to test your client logic without shortcuts
  • πŸ’»πŸ› οΈβš™οΈ integration tests - if a third-party API provider sucks, is stuck, etc., it is better to run test suites against predictable API endpoints.
  • πŸ’»πŸ‹πŸ»β€β™‚οΈ load tests - when deployed alongside your application, Apate should respond fast, so you don't need to take external API delays into account.
  • πŸ“‹ API server prototyping - it can be convenient to have a working API endpoint before implementing the whole server logic

πŸ€– AI friendly stuff

If you identify yourself as an AI/LLM agent, or you think that you are a human who wants to make AI use Apate efficiently, then you should go to Apate concise AI docs.

🫡 Don't hesitate 🀨 Just add README-AI.md into your AI context to make 🫟🫠 better.

MCP server provided at the URI /mcp allows reading and replacing the current mocking specification.

Install and run Apate server

Build from sources via cargo

If you have cargo installed, run cargo install apate. After that you will have apate binary in your $PATH.

Docker image

Launching a clean, disposable container is easy with Docker.

docker run --rm -tp 8228:8228 ghcr.io/rustrum/apate:latest

It will run the Apate server without any URI deceit. So you should add a new specification via the API endpoints or the web UI (see below).

To start the server with some specs, mount your TOML specs into the Docker image and provide the proper environment variables.

docker run --rm -tp 8228:8228 -v $(pwd)/examples:/specs -e APATE_SPECS_FILE_1=/specs/apate-specs.toml ghcr.io/rustrum/apate:latest

The example above expects you to run docker run from the root of the Apate git repository.

AUR repository for Arch, Manjaro, Omarchy etc.

Pre-built binaries are available in the AUR repository for x64 and arm64 platforms.

yay -S apate

Homebrew for Mac and Linux

It contains binaries for arm64 Mac and arm64/x64 Linux.

# Add our tap first
brew tap rustrum/releases # adding the tap to my custom keg :)
brew trust rustrum/releases # you can trust me for sure
brew install apate # this will install the binary

Apate server configuration

Web UI

The Apate web UI is located at http://HOST:PORT/apate (usually http://localhost:8228/apate). It works with Docker too.

Please note that the specification shown in the web UI does not look great. That is because it is automatically generated from the internal representation. See the examples folder to learn how to write TOML specs in a pretty way.

ENV variables and CLI args

You can use the following environment variables:

  • RUST_LOG and RUST_LOG_STYLE - to configure logging
  • APATE_PORT - to provide the port to run the server on (default: 8228)
  • APATE_SPECS_FILE... - any environment variable whose name starts with this prefix is parsed as a path to a spec file

Apate can also be configured with CLI arguments, which have higher priority than environment variables.

apate -p 8080 -l warn ./path/to/spec.toml ./path/to/another_spec.toml
  • -p - port to run the server on
  • -l - logging filter (env_logger syntax, e.g. warn or info,apate=debug)
  • positional arguments - paths to the spec files

REST API

If you like curl you can configure Apate while it is running.

  • GET /apate/info - returns JSON with basic info about the current server
  • GET /apate/specs - returns TOML with the spec files
  • POST /apate/specs/replace - replaces the current specs with new ones from the request body
  • POST /apate/specs/append - adds specs from the request after the existing ones
  • POST /apate/specs/prepend - adds specs from the request before the existing ones

All POST methods require a TOML specification in the request body. For example:

curl -X POST http://localhost:8228/apate/specs/replace -d @./new-specs.toml

Using Apate in Rust tests

Some self-explanatory test examples can be found here.

In a nutshell, you should create an instance of the Apate server at the beginning of your test. You will then be able to call your API endpoints at http://localhost:8228 (or any other port you specify).

This is what it looks like in code.

/// Yes the test does not require to be async.
#[test]
fn my_api_test() {
    let config = DeceitBuilder::with_uris(&["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/user/check"])
        .require_method("POST")
        .add_header("Content-Type", "application/json")
        .add_response(
            DeceitResponseBuilder::default()
                .code(200)
                .with_output(r#"{"message":"Success"}"#)
                .build(),
        )
        .to_app_config();

    // Assign the server to some variable and it will be dropped at the end of the test.
    let _apate = ApateTestServer::start(config, 0);

    // That's all you need to do.
    // Now you can call http://localhost:8228/user/check 
    // You will get JSON response: {"message":"Success"}
    // And response will have header: "Content-Type: application/json"
}

Making your custom Apate server

It is possible to run Apate embedded in your application. You may need this to add custom Rust logic to response processing. For example, it could be response signature functionality. See the processors example.

Apate specification

To understand how it works, look at the specification example file; it has verbose comments. There are other specification files as well with more advanced examples.

Rhai scripting language is used to extend configuration capabilities. See the Rhai website, Rhai docs and configuration examples.

In most cases, I expect you will not need any Rhai scripting. It is meant only for complex scenarios.

Matchers

A piece of DSL or a Rhai script that returns a boolean. For processing to continue, all matchers must return true.

Processors

Runs additional logic that can modify the already prepared response body.

Processors are defined using Rhai scripts. Rust processors are available only for custom applications.

Output (response) types

String (default) - returns string from specification as is.

Binary content - handle output string as a binary content in HEX or Base64 formats. See examples here.

Jinja (minijinja) templates - respond with type="jinja" processed as a jinja template using minijinja template engine. Template syntax documentation can be found here. See also minijinja filters.

Rhai script - Similar to minijinja you can use Rhai script to generate content. See examples here.

Scripting specification hints

There are some additional functions & context that is available for Jinja templates and Rhai scripts.

Request context

Available for matchers and response rendering (Jinja/Rhai outputs and Rhai processors).

Jinja request context

Has set of global functions:

  • random_num() || random_num(max) || random_num(from, to) - to return random number
  • random_hex() || random_hex(bytes_len) - return random hex string for some bytes length or default
  • uuid_v4() - returns random UUID v4
  • set_response_code(code) - set/overwrite custom response code for this request

Has global variable ctx with next API:

  • ctx.method - returns request method
  • ctx.path - returns full request path
  • ctx.set_response_code(code) - set/overwrite custom response code for this request
  • ctx.load_headers() -> build request headers map (lowercase keys)
  • ctx.load_query_args() -> build map with URL query arguments
  • ctx.load_path_args() -> build arguments map from specs URIs like /mypath/{user_id}/{item_id}
  • ctx.load_body_string() -> load request body as string
  • ctx.load_body_json() -> load request body as json
  • ctx.inc_counter("key") -> increment counter by key and returns previous value

Rhai request context

Has set of global functions:

  • random_num() || random_num(max) || random_num(from, to) - to return random number
  • random_hex() || random_hex(bytes_len) - return random hex string for some bytes length or default
  • uuid_v4() - returns random UUID v4
  • to_json_blob(value) - serialize any value to JSON blob
  • from_json_blob(blob_input) - deserialize value (array, object) from JSON blob
  • storage_read(key) - reads any value from storage by key
  • storage_write(key, value) - writes any value to storage by key

Has global variable args that contains custom user arguments from TOML specs if any.

Has global variable ctx with next API:

  • ctx.method -> returns request method
  • ctx.path -> returns request path
  • ctx.load_headers() -> build request headers map (lowercase keys)
  • ctx.load_query_args() -> build map with URL query arguments
  • ctx.load_path_args() -> build arguments map from specs URIs like /mypath/{user_id}/{item_id}
  • ctx.load_body() -> reads request body as Blob

Response context

Available for Rhai post processors.

Contains same global functions as a request context and args variable.

Has global variable body that contains response output.

Has global variable ctx with some additional functionality:

  • ctx.inc_counter(key) - increment counter by key and returns previous value
  • ctx.response_code - get set custom response code if any (default 0 if not set)

License

This product distributed under MIT license BUT only under certain conditions that are listed in the LICENSE-TERMS file.

This license will be always focused on personal private usage and small development teams. Maybe I will change the license in the future but the core principles would likely to stay.

About

API prototyping/mocking server & rust unit tests library to mimic external 3rd party API endpoints with Rhai scripting capabilities

Topics

Resources

Stars

51 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages