Embed a Make Bold Spark API testing workbench into any .NET 10 Minimal API project. Autodiscovers your OpenAPI v3 endpoints. No separate deployment required.
Install from NuGet
dotnet add package ApiTestSpark
MIT license · net10.0 · 500 KB · No dependencies · Last updated June 12, 2026
API Test Spark embeds a React-powered test harness directly into your .NET application. No extra services, no configuration files, no separate deployments.
Points at your OpenAPI v3 document and renders every endpoint in a collapsible accordion grouped by tag. Works with .NET's built-in MapOpenApi().
The entire React SPA is embedded as resources inside the package. No wwwroot copying, no CDN, no build step required in your app.
Pre-populate Bearer tokens, API keys, or custom headers for every request. Supports Bearer, ApiKey, and Basic schemes.
Inspect every request, response, and error in real time. cURL snippet generation per request. FIFO history buffer keeps memory bounded.
Select endpoints, capture live curl + responses, annotate sections, and export a complete markdown document for front-end developer agents — at /api-docs.
Restrict the harness to specific environments such as Development or Staging. One option keeps the harness out of production entirely.
Set EnableDemoIntegrations = false to hide the built-in JokeAPI and JSONPlaceholder screens. Present a clean harness showing only your host API and API Doc Builder — no sample data, no external noise.
Depth-1 nested object fields in API responses render as collapsible editable sub-forms. Edit a nested value and click "Copy as JSON" to get updated output — without leaving the tool.
One-click cURL command generation is now available in the response panel as well as the request panel. The command always captures the request that produced the response shown.
Switch between 2-space-indented and single-line JSON views for any raw JSON response. The preference persists across API calls for the browser session and resets on page reload.
Every field in the response form shows its dot-notation JSONPath address ($.field, $.parent.field) as a tooltip. Click any field label to copy the path to the clipboard.
Browse and test multiple named remote REST APIs from their OpenAPI documents. Configure RemoteApiProfiles in Program.cs or add browser-local profiles from the Config page.
The embedded UI ships with Make Bold Solutions colors, logo assets, favicon set, package icon, and Inter Tight typography as part of the Make Bold Spark product family.
A server-side proxy endpoint (GET /api-test-spark/remote-spec?profileId=...) resolves server profiles by id and injects API keys or Bearer tokens without serializing secrets to the browser.
Header values support {session-guid} (one UUID per page load) and {request-guid} (fresh UUID per call), expanded at request-send time. Ideal for correlation IDs in distributed tracing.
The same endpoint-capture and markdown-export experience as the host API Doc Builder, but scoped to the selected remote profile so generated docs use that profile's name and description.
From zero to a running test harness in under five minutes.
Run this in your project directory or use the NuGet Package Manager.
dotnet add package ApiTestSpark
.NET 9+ includes OpenAPI support built-in. Add it to the service container and map the document endpoint.
var builder = WebApplication.CreateBuilder(args); builder.Services.AddOpenApi(); // built-in .NET 9+ var app = builder.Build(); app.MapOpenApi(); // serves at /openapi/v1.json
Call MapApiTestSpark() after building the app. That's all that's required.
app.MapApiTestSpark();
The harness is now live at https://localhost:{port}/api-test-spark/
A complete working example — copy, paste, run.
using ApiTestSpark; var builder = WebApplication.CreateBuilder(args); builder.Services.AddOpenApi(); var app = builder.Build(); app.MapOpenApi(); app.MapGet("/products", () => new[] { new { Id = 1, Name = "Widget", Price = 9.99 } }).WithSummary("List all products"); app.MapApiTestSpark(options => { options.OpenApiUrl = "/openapi/v1.json"; options.Environments = ["Development"]; options.EnableDemoIntegrations = false; // hide demos, show only your API }); app.Run();
All options are set via the Action<ApiTestSparkOptions> delegate passed to MapApiTestSpark().
Every property has a sensible default — only set what you need.
| Property | Default | Description |
|---|---|---|
| OpenApiUrl | "/openapi.json" | Relative or absolute URL to your OpenAPI v3 JSON document. The SPA fetches this on startup to discover endpoints. Set to null to disable autodiscovery. |
| AuthScheme | null | Advertises the auth scheme to the SPA UI ("Bearer", "ApiKey", or "Basic"). Pre-populates the auth field. Never a token value. |
| DefaultHeaders | {} | Key-value headers injected into every request the SPA makes to your API. Use for tenant IDs, correlation headers, etc. Must not contain secrets — values are served via the public config endpoint. |
| Environments | [] (all) | Environment names where the harness is active. Empty array enables it everywhere. Example: ["Development", "Staging"] keeps it off production. |
| CorsOrigins | [] (same-origin) | Extra origins allowed to call the config endpoint. Use when the Vite dev server and your .NET API run on different ports, e.g. ["http://localhost:5151"]. |
| EnableVerboseLogging | false | Emits ILogger.LogDebug for every static asset served and every SPA fallback. Alternatively set Logging:LogLevel:ApiTestSpark=Debug in appsettings without redeploying. |
| EnableDemoIntegrations | true | When false, hides the built-in JokeAPI and JSONPlaceholder demo screens from the home page and disables their routes (/joke-api, /json-placeholder). The home page shows only Host API Explorer and API Doc Builder. Default true — existing installs are unaffected. |
| RemoteApiProfiles | [] | List of named remote API defaults. Each profile includes an id, name, description, base URL, OpenAPI URL, credentials, and default headers. |
| RemoteBaseUrl | null | Legacy single-remote base URL. Used as one compatibility profile when RemoteApiProfiles is empty. |
| RemoteOpenApiUrl | null | Legacy single-remote OpenAPI URL. Used as one compatibility profile when RemoteApiProfiles is empty. |
| RemoteOpenApiApiKeyHeader | null | Legacy API key header name for the compatibility profile. |
| RemoteOpenApiApiKeyValue | null | Legacy API key value. Used server-side only and redacted from /api-test-spark/config. |
| RemoteOpenApiBearerToken | null | Legacy bearer token. Used server-side only and redacted from /api-test-spark/config. |
| RemoteDefaultHeaders | {} | Legacy headers injected into every browser-side request to the compatibility remote API. Supports {session-guid} and {request-guid} tokens. |
app.MapApiTestSpark(options => { options.OpenApiUrl = "/openapi/v1.json"; options.AuthScheme = "Bearer"; options.DefaultHeaders["X-Tenant-Id"] = "acme"; options.Environments = ["Development", "Staging"]; });
Configure named remote API profiles. Server profile specs are fetched by profile id so API key values stay server-side and redacted from the config payload.
app.MapApiTestSpark(options => { options.OpenApiUrl = "/openapi/v1.json"; options.RemoteApiProfiles.Add(new RemoteApiProfile { Id = "partner-api", Name = "Partner API", Description = "External partner integration endpoints.", RemoteBaseUrl = "https://api.partner.com", RemoteOpenApiUrl = "https://api.partner.com/openapi.json", RemoteOpenApiApiKeyHeader = "x-api-key", RemoteOpenApiApiKeyValue = "your-api-key", // stays server-side RemoteDefaultHeaders = { ["correlationId"] = "{request-guid}", ["sessionId"] = "{session-guid}", }, }); });
Call UseForwardedHeaders() before MapApiTestSpark() so the config endpoint reports the correct public base URL.
app.UseForwardedHeaders(); app.MapApiTestSpark();
This server is the live demonstration. Three related API groups are running and fully annotated — Products, Customers, and Orders. API Test Spark has autodiscovered all of them via the OpenAPI document.
Click to open the harness. Use the collapsible group list on the left to navigate between resource groups. Request body fields are pre-filled from the schema. Responses render as sortable tables or editable forms in the debug panel.
⚡ Open API Test Spark/products/products/{id}/products/products/{id}/products/{id}/customers/customers/{id}/customers/customers/{id}/customers/{id}/orders/orders/{id}/orders/customer/{id}/orders/orders/{id}/status/orders/{id}
API Test Spark compiles the React SPA into embedded resources inside the NuGet package.
When you call MapApiTestSpark(), the library registers four things into your ASP.NET Core pipeline:
Serves the embedded SPA assets (HTML, JS, CSS, icons) at /api-test-spark/ using EmbeddedFileProvider. No files are copied to your project. No wwwroot changes.
Registers GET /api-test-spark/config. The SPA fetches this on startup to receive all configuration — host API URL, auth scheme, default headers, redacted remote API profile metadata, and the harness version/build date. Nothing is hardcoded in the bundle.
{
"baseUrl": "https://your-api.example.com",
"openApiUrl": "/openapi/v1.json",
"authScheme": "Bearer",
"defaultHeaders": { "X-Tenant-Id": "acme" },
"enableDemoIntegrations": true,
"remoteApiProfiles": [
{
"id": "partner-api",
"name": "Partner API",
"description": "External partner integration endpoints.",
"remoteBaseUrl": "https://api.partner.com",
"remoteOpenApiUrl": "https://api.partner.com/openapi.json",
"remoteOpenApiApiKeyValue": null,
"remoteOpenApiApiKeyConfigured": true,
"source": "server",
"proxyMode": "server"
}
],
"harnessVersion": "1.5.0",
"harnessBuiltAt": "2026-06-09T01:24:11Z"
}
Registers GET /api-test-spark/remote-spec?profileId=.... When a server-configured Remote API Explorer loads, the SPA requests this endpoint by profile id. The proxy resolves only server-provided profiles, injects API keys or Bearer tokens server-side, fetches the remote OpenAPI document, validates it, and returns the JSON. Browser-created profiles fetch their OpenAPI documents directly from the browser.
Extensionless paths under /api-test-spark/ fall back to index.html so client-side routing works. Requests for unknown file extensions return HTTP 404 — the SPA never silently swallows asset 404s.
API Test Spark's entire input is your OpenAPI v3 document. Everything it renders — endpoint groups, descriptions, request scaffolds, response schemas, status codes — comes directly from that document. The richer your OpenAPI metadata, the better your test harness. This page is itself a live example: every section below is demonstrated by the running Products, Customers, and Orders API. Open the harness alongside this guide to see each technique in action.
| OpenAPI feature | What API Test Spark does with it | Impact |
|---|---|---|
operation tags with "Namespace: Label" format | Two-level collapsible accordion groups on the left nav | High |
operation summary | Bold title shown on every endpoint card | High |
operation description (markdown) | Rendered markdown below the summary — bold, lists, code, tables | High |
operationId / WithName() | Copyable chip beside each endpoint; used in API Doc Builder references | High |
request body schema with example / default | JSON scaffold pre-filled in the request body editor | High |
schema property description | Shown in the schema property table beside each field | High |
Produces<T> per status code | Coloured response-code badges with expandable inline schemas | High |
info.title, info.version, info.contact | API info header at the top of the Host API screen | Medium |
info.description (markdown) | Rendered in the API info header — ideal for workflow walkthroughs | Medium |
parameter description + example | Shown in the parameter table; example pre-fills path/query fields | Medium |
schema constraints (minLength, maximum, enum) | Displayed in schema property tables; enum drives a select input | Medium |
deprecated: true | Endpoint visually flagged as deprecated in the accordion | Low |
info.license | Shown in the API info header | Low |
API Test Spark parses tags in "Namespace: Label" format into a two-level accordion. Without this pattern all endpoints land in a single flat list.
WithTags("Products: Catalog") on your route group[Tags("Orders: Lifecycle")] on a controller"Auth: Tokens" and "auth: tokens" become separate groupssummary is the title shown on every card. description accepts full markdown and renders inline — use it to explain behaviour, constraints, and cross-references.
**text**), inline code, bullet lists, fenced code blocks, and tables all renderWithName()operationId (set via WithName() on Minimal APIs) is surfaced as a copyable chip on each endpoint card and used as the section heading in exported API Doc Builder documents.
GetProductById, CreateOrder, UpdateOrderStatusoperationId automaticallyProduces<T>Each .Produces<T>(statusCode) call adds a coloured badge in the harness. Click the badge to expand the inline schema. Undeclared status codes produce no badge — testers have to guess.
Produces<string>(400) or a problem-details typeTypedResults — it declares response types automatically without extra .Produces() calls[ProducesResponseType] attributesAPI Test Spark renders a property table for every request body and response schema. [Description], [Range], [MinLength], and [MaxLength] all appear as columns in that table.
[Description("...")] from System.ComponentModel to every public property[Range(min, max)] for numeric bounds — displayed in the constraints column[MinLength] / [MaxLength] for string lengths[Required] — surfaced as a required marker in the tableenum types render as a select input in the scaffold editorAPI Test Spark pre-fills the JSON scaffold from example → default → enum[0] → type placeholder. Without examples, every field shows a generic placeholder. With examples, testers can run requests immediately.
[DefaultValue("acme")] to pre-fill string fieldsint StockQuantity = 0WithOpenApi(op => { op.RequestBody.Content["application/json"].Example = ... }) for a full example bodyinfo.description for workflow documentationThe API-level info.description field renders as markdown in the Host API screen header. Use it to describe the overall API, link resource groups together, and provide a step-by-step workflow for testers.
AddDocumentTransformer in AddOpenApi()Avoid these patterns — they leave testers with empty or misleading harness UI.
IResult without TypedResults — response type information is lost; use Results<Ok<T>, NotFound> insteadThe difference between a bare endpoint and a fully-annotated one.
// No tags, no name, no summary, // no description, no response type app.MapGet("/products/{id}", (int id, ProductCache cache) => cache.GetById(id) ?? (IResult)Results.NotFound() );
app.MapGet("/products/{id}", GetById) .WithName("GetProductById") .WithSummary("Get a product by ID") .WithDescription("Returns a single product. " + "Seeded IDs are **1–10**. " + "Returns 404 if not found.") .Produces<Product>(200) .Produces(404); // Handler uses TypedResults so the // return type is inferred automatically static Results<Ok<Product>, NotFound> GetById(int id, ProductCache cache) => cache.GetById(id) is { } p ? TypedResults.Ok(p) : TypedResults.NotFound();
The same principles apply — just use attributes instead of fluent calls.
/// <summary>Get a product by ID.</summary> /// <remarks>Seeded IDs are **1–10**. Returns 404 if not found.</remarks> [HttpGet("{id}")] [Tags("Products: Catalog")] [ProducesResponseType(typeof(Product), 200)] [ProducesResponseType(404)] public ActionResult<Product> GetById(int id) { ... }
Every best practice above is implemented in this demo. The Products, Customers, and Orders source code is available in the SampleApi folder on GitHub. Open the harness and compare what you see against the source to understand exactly what each annotation produces.
⚡ Open the harnessBrowse and test named remote REST APIs from their OpenAPI documents without leaving the harness. Server-configured profile specs use a credential-safe proxy; browser-created profiles are stored locally and fetch specs directly.
For server profiles, the browser calls GET /api-test-spark/remote-spec?profileId=... → .NET resolves the server profile → adds credentials → fetches remote OpenAPI JSON. Credential values are redacted from config.
When you click Send, the request goes from your browser to the selected profile's remote server. Default headers, browser-local credentials, and token placeholders such as {request-guid} are injected by the SPA.
Server profiles appear first and can be hidden locally. Browser profiles are added, edited, and deleted from the Config page, with all browser-managed values persisted in localStorage.
This site seeds two remote profiles from Program.cs: JSONPlaceholder and the hosted API Test Spark demo. Open either profile's explorer or docs from the harness home screen.
app.MapApiTestSpark(options => { options.OpenApiUrl = "/openapi/v1.json"; options.RemoteApiProfiles.Add(new RemoteApiProfile { Id = "jsonplaceholder-demo", Name = "JSONPlaceholder", Description = "Public demo API for posts, users, and comments.", RemoteBaseUrl = "https://jsonplaceholder.typicode.com", RemoteOpenApiUrl = "https://apitest.makeboldspark.com/openapi/v1.json", RemoteDefaultHeaders = { ["correlationId"] = "{request-guid}", ["sessionId"] = "{session-guid}", }, }); });
API Test Spark ships frequently. Every release is a backwards-compatible drop-in upgrade.
Make Bold Solutions brand alignment. API Test Spark now presents as a Make Bold Spark product across the embedded UI, favicon set, package icon, NuGet metadata, package README, and public documentation. No public API changes.
Remote API Profiles. Configure multiple named remote APIs in Program.cs or from the browser Config page. Each profile has its own explorer and doc builder route, safe name/description display, scoped headers and credentials, redacted server secrets, server-profile-only proxying, local browser-created profiles, and duplicate-name validation before save.
Remote API Explorer. Browse and test remote REST APIs from named RemoteApiProfiles. Server-side spec proxy resolves server profile ids and redacts credential values from config; browser-created profiles stay local and fetch specs directly. Header token expansion ({session-guid}, {request-guid}). Harness version and build date on About page.
Response panel DX improvements. Editable depth-1 nested object sub-forms (collapsed by default, values merge into "Copy as JSON"). "Copy as cURL" in the response panel. Pretty/minified JSON toggle with session-persistent preference. JSONPath tooltips on every field label (click to copy). 2-row table truncation with show-all/show-less. buildCurl extracted to shared src/utils/curlBuilder.ts.
Demo integration toggle. New EnableDemoIntegrations option — set to false to hide the built-in JokeAPI and JSONPlaceholder demo screens. TypeScript type system hardened: ErrorCategory union expanded with 'React'; ErrorBoundary observability corrected. Constitution amended to v1.1.1.
CSP fix. Fixed Content-Security-Policy blocking localhost WebSocket and HTTP connections in Development, restoring Browser Link and hot-reload. No public API changes.
API Doc Builder + rich metadata. New /api-docs screen captures live endpoint responses and exports complete markdown documentation. Full OpenAPI metadata surface: response codes with inline schemas, operationId chip, schema constraints, markdown rendering, API info header. Relational seed data in SampleApi.
Initial release. MapApiTestSpark() extension, OpenAPI v3 autodiscovery, collapsible accordion endpoint groups, smart response rendering (tables/forms/pre), cURL generation, debug panel, environment gating, Azure Application Insights integration, 30 MSTest integration tests.
The package targets net10.0. For earlier targets, reference the package source directly and adjust the target framework in the .csproj. OpenAPI support is built-in from .NET 9 onwards; for .NET 8 use Swashbuckle and point OpenApiUrl at your Swagger JSON URL.
The config endpoint is publicly accessible and returns metadata (auth scheme, header names) — never token values. For production we recommend using Environments = ["Development", "Staging"] or adding network-level access controls (e.g. IP allowlist on your reverse proxy) to restrict access.
The harness is scoped to /api-test-spark/. It does not affect any other routes or middleware. If you have a WAF or CDN, note that all extensionless paths under /api-test-spark/ return HTTP 200 — the React router handles 404s client-side.
Set options.Environments = ["Development"]. The library checks IHostEnvironment.EnvironmentName at startup and skips registration if the current environment is not in the list.
Only OpenAPI v3.x documents are parsed. For Swagger 2.0 APIs, use a converter to produce a v3 document (e.g. converter.swagger.io) and point OpenApiUrl at the converted output.
Yes — the API Doc Builder at /api-docs lets you select endpoints, capture live requests and responses, annotate sections, and export a complete markdown document targeted at front-end developer agents. It includes exact curl commands, full JSON responses, parameter tables, and schema tables.
Yes — set options.EnableDemoIntegrations = false when calling MapApiTestSpark(). The home page will show only the Host API Explorer and API Doc Builder, and the demo routes (/joke-api, /json-placeholder) are disabled entirely. This is the recommended setting for teams using API Test Spark to test their own APIs rather than as a general-purpose demo tool.
This demo site sets EnableDemoIntegrations = true so you can explore all features including the built-in JokeAPI and JSONPlaceholder integrations. In a real installation you would typically set this to false to present a focused harness for your own API.
For server-configured profiles, the SPA calls GET /api-test-spark/remote-spec?profileId=... — a .NET endpoint in the same process as your app. That endpoint resolves only server-provided profile ids, reads that profile's API key or Bearer token from ApiTestSparkOptions (server-side memory, never the browser), and injects them into the outbound spec request. Browser-created profiles do not use the proxy; they fetch OpenAPI documents directly from the browser.
These are token placeholders you can embed in any header value in RemoteDefaultHeaders. {session-guid} is replaced with one UUID that stays constant for the entire page session — useful for tracking a user's full session in a distributed trace. {request-guid} is replaced with a fresh UUID on every individual API call — useful as a per-request correlation ID. Expansion happens at request-send time, not at configuration time.
Yes, if the remote server allows browser calls. Server-configured profiles add their RemoteBaseUrl to the page's Content-Security-Policy connect-src directive, and browser-created profiles are allowed by the harness CSP as well. The remote server still needs permissive CORS headers for browser-direct endpoint calls and browser-created OpenAPI spec fetches.
The proxy accepts server-provided profile ids only. Browser-created profile credentials are persisted in localStorage and applied to browser-direct spec fetches and endpoint calls, but they are never submitted to /api-test-spark/remote-spec. This keeps the proxy from becoming an arbitrary server-side URL fetcher and keeps browser-local secrets out of the proxy request.