Embed an interactive API test harness into any .NET 10 Minimal API with one line of code.
NuGet Package · Live Demo · Make Bold Spark · GitHub Repository · Report an Issue
API Test Spark is a Make Bold Spark NuGet package, fully owned and managed by Make Bold Solutions. It embeds a full-featured, React-powered API test harness directly into any .NET 10 Minimal API application. Drop in one method call — MapApiTestSpark() — and your API instantly gains an interactive testing UI at /api-test-spark/.
No separate deployment. No Swagger UI dependency. No wwwroot changes. The entire branded React SPA ships as embedded resources inside the package.
dotnet add package ApiTestSpark
See it live at https://apitest.makeboldspark.com — the demo runs against a real .NET 10 Minimal API with 16 endpoints across Products, Customers, and Orders.
- Added
RequireAuthenticatedUserso teams can require authentication for all harness routes under/api-test-spark. - Added server-side expansion of
{request-guid}and{session-guid}in proxied remote-call headers. - Hardened config endpoint CORS behavior by emitting
Vary: OriginwhenAccess-Control-Allow-Originis set. - Updated CI and publish workflows to run
npm run verify(lint + typecheck + build) for fail-fast frontend validation. - Fixed remote spec proxy resource management (disposal,
ConfigureAwait(false),ArrayPoolbuffering). - Fixed sticky namespace header overlapping first endpoint in the endpoint list.
See CHANGELOG.md for full details.
| Property | Value |
|---|---|
| Package ID | ApiTestSpark |
| Version | 1.8.0 |
| Authors | Make Bold Solutions; Mark Hazleton |
| Company | Make Bold Solutions |
| License | MIT |
| Target Framework | net10.0 |
| Package Size | 0.49 MB |
| Symbol Package | 15.7 KB (.snupkg) |
| Dependencies | None |
| Last Updated | June 21, 2026 |
| NuGet | nuget.org/packages/ApiTestSpark |
| Live Demo | apitest.makeboldspark.com |
| Product Family | Make Bold Spark |
| Source | github.com/MarkHazleton/ApiTestSpark |
dotnet add package ApiTestSparkOr via the NuGet Package Manager:
Install-Package ApiTestSparkusing ApiTestSpark;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi(); // built-in .NET 9+
var app = builder.Build();
app.MapOpenApi(); // exposes /openapi/v1.json
app.MapGet("/products", () => new[] {
new { Id = 1, Name = "Widget", Price = 9.99 }
}).WithSummary("List all products");
app.MapApiTestSpark(); // harness live at /api-test-spark/
app.Run();Navigate to https://localhost:{port}/api-test-spark/ — API Test Spark autodiscovers all your OpenAPI v3 endpoints and renders them ready to test.
| Feature | Description |
|---|---|
| OpenAPI Autodiscovery | Points at your OpenAPI v3 document and renders every endpoint in a collapsible accordion, grouped by tag |
| Zero Configuration | Works with .NET's built-in MapOpenApi() — sensible defaults for every option |
| No Dependencies | The React SPA is compiled into embedded resources; nothing is copied to your project |
| Auth & Header Injection | Pre-populate Bearer tokens, API keys, or custom headers for every request |
| Request Body Scaffolding | JSON body pre-filled from schema example → default → enum[0] → type placeholder |
| Full OpenAPI Metadata | Descriptions rendered as markdown, response codes as coloured badges with expandable schemas |
| Editable Nested Responses | Depth-1 nested object fields in responses render as editable sub-forms; edited values merge into "Copy as JSON" |
| Copy as cURL | One-click cURL command for any completed API call — available on both the request and response panels |
| Pretty / Minified Toggle | Toggle between 2-space-indented and single-line JSON views; preference persists for the browser session |
| JSONPath Field Labels | Every response field shows its dot-notation JSONPath address as a tooltip — click to copy |
| Table Truncation | Large array responses show 2 rows by default with a "Show all N items" expand/collapse control |
| API Doc Builder | Select endpoints, capture live curl + responses, annotate, and export markdown at /api-docs |
| Remote API Profiles | Configure multiple named remote APIs in Program.cs or the browser, each with its own explorer and doc builder route |
| Server-side Remote Call Proxy | Opt-in EnableRemoteCallProxy routes server-configured remote endpoint calls through your host app to avoid browser CORS requirements |
| Identity-Aware Header Templates | Expand {user-name}, {user-email}, and {user-id} in configured host and remote headers using the authenticated user context |
| Live Debug Panel | Every request, response, error, and performance metric — drag-resizable, FIFO buffered |
| Environment Gating | Restrict the harness to Development or Staging; keep it off production with one option |
| Demo Integration Toggle | Hide the built-in JokeAPI and JSONPlaceholder demo screens with one option — show only your host API |
| Source Link | Step-through debugging into the package source from Visual Studio / Rider |
All options are set via the Action<ApiTestSparkOptions> delegate:
app.MapApiTestSpark(options =>
{
options.OpenApiUrl = "/openapi/v1.json"; // default: "/openapi.json"
options.AuthScheme = "Bearer"; // "Bearer" | "ApiKey" | "Basic" | null
options.DefaultHeaders["X-Tenant-Id"] = "acme";
options.Environments = ["Development", "Staging"];
options.CorsOrigins = ["http://localhost:5151"];
options.EnableVerboseLogging = false;
options.EnableDemoIntegrations = false; // hide JokeAPI + JSONPlaceholder demos
});| Option | Default | Description |
|---|---|---|
OpenApiUrl |
"/openapi.json" |
Relative or absolute URL of your OpenAPI v3 JSON document |
AuthScheme |
null |
Advertises the auth scheme to the UI — never a token value |
DefaultHeaders |
{} |
Headers injected into every request the SPA sends to your API |
Environments |
[] (all) |
Environment names where the harness is active; empty = everywhere |
CorsOrigins |
[] |
Extra origins allowed to call the config endpoint |
EnableVerboseLogging |
false |
Emits ILogger.LogDebug for every asset served and SPA fallback |
EnableDemoIntegrations |
true |
When false, hides the built-in JokeAPI and JSONPlaceholder demo screens and disables their routes. Set to false for a clean harness showing only your host API. |
RequireAuthenticatedUser |
false |
When true, all harness routes under /api-test-spark require an authenticated user (SPA assets + config + remote proxy endpoints). |
RemoteApiProfiles |
[] |
Named remote API defaults. Each profile has an id, name, description, base URL, OpenAPI URL, credentials, and headers. |
EnableRemoteCallProxy |
false |
Routes endpoint calls for server-configured remote profiles through /api-test-spark/remote-call, avoiding browser CORS requirements while keeping server-held credentials off the client. |
RemoteBaseUrl / RemoteOpenApiUrl |
null |
Legacy single-remote options. When RemoteApiProfiles is empty, these seed one compatibility profile. |
Use these tokens only in explicitly configured request headers. They are resolved when
/api-test-spark/config is requested and unresolved values become an empty string.
| Token | Resolution order |
|---|---|
{user-name} |
Identity.Name -> name -> preferred_username |
{user-email} |
ClaimTypes.Email -> email -> preferred_username |
{user-id} |
ClaimTypes.NameIdentifier -> sub -> oid |
For server-proxied remote calls (EnableRemoteCallProxy = true), RemoteDefaultHeaders
also supports {request-guid} and {session-guid}. The server resolves these values
per request before forwarding to the remote API.
preferred_username is used as an email fallback only when the identity provider
guarantees that it contains an email address or UPN.
app.MapApiTestSpark(options =>
{
options.RemoteApiProfiles.Add(new RemoteApiProfile
{
Id = "orders-api",
Name = "Orders API",
Description = "Order management endpoints.",
RemoteBaseUrl = "https://orders.example.com",
RemoteOpenApiUrl = "https://orders.example.com/openapi.json",
RemoteOpenApiApiKeyHeader = "x-api-key",
RemoteOpenApiApiKeyValue = builder.Configuration["Orders:ApiKey"],
});
});Server profile secrets are redacted from GET /api-test-spark/config. The server proxy fetches specs by profile id at GET /api-test-spark/remote-spec?profileId=orders-api, so browser-created profiles cannot submit arbitrary URLs to the server proxy. When EnableRemoteCallProxy = true, endpoint calls for server-configured profiles are also routed through GET|POST|PUT|PATCH|DELETE /api-test-spark/remote-call?profileId=...&path=.... Browser-created profiles are stored in localStorage, fetch OpenAPI documents directly from the browser, and keep their endpoint calls browser-direct.
https://apitest.makeboldspark.com is the official demo and product site for API Test Spark. It runs on .NET 10 with ApiTestSpark v1.8.0 installed and exposes 16 real endpoints:
| Group | Endpoints |
|---|---|
| Products | GET /products, GET /products/{id}, GET /products/categories, GET /products/category/{name}, POST /products, PUT /products/{id}, DELETE /products/{id} |
| Customers | GET /customers, GET /customers/{id}, POST /customers, PUT /customers/{id}, DELETE /customers/{id} |
| Orders | GET /orders, GET /orders/{id}, GET /orders/customer/{customerId}, GET /orders/status/{status}, POST /orders, PATCH /orders/{id}/status, DELETE /orders/{id} |
Open the harness directly: https://apitest.makeboldspark.com/api-test-spark/
MapApiTestSpark() registers five things into your ASP.NET Core pipeline:
- Static file middleware — serves the embedded SPA assets (HTML, JS, CSS) from
EmbeddedFileProviderat/api-test-spark/. No files are copied to your project. - Config endpoint —
GET /api-test-spark/configreturns yourOpenApiUrl,AuthScheme,DefaultHeaders, resolveduserName/userEmail/userId, redacted remote API profile metadata, and harness version at runtime. The SPA fetches this on startup — no values are hardcoded in the bundle. - Remote spec proxy —
GET /api-test-spark/remote-spec?profileId=...fetches the configured server remote profile OpenAPI document and returns the JSON to the SPA. API key and Bearer token are injected at the server; credential values are not serialized to the browser config payload. - Remote call proxy — when
EnableRemoteCallProxyis enabled, endpoint calls for server-configured remote profiles are forwarded throughGET|POST|PUT|PATCH|DELETE /api-test-spark/remote-call?profileId=...&path=..., avoiding browser CORS requirements and keeping server-held credentials out of the browser. - SPA fallback — extensionless paths under
/api-test-spark/serveindex.htmlso client-side routing works. Unknown file extensions return HTTP 404.
Auth gate + proxy hardening: new RequireAuthenticatedUser option enforces authentication on all harness routes (/api-test-spark/ SPA assets, config, remote-spec, and remote-call). The remote spec proxy now properly disposes HttpResponseMessage, uses ConfigureAwait(false) throughout, and buffers responses via ArrayPool. Config endpoint CORS responses now include Vary: Origin for correct cache behavior. CI workflows gain a fail-fast npm run verify frontend gate before .NET steps. Fixed: sticky namespace header overlapping the first endpoint item in the left panel.
Remote call proxy + identity-aware headers: server-configured remote API profiles can now route endpoint calls through the host app with EnableRemoteCallProxy, avoiding browser CORS issues while keeping server-held credentials off the client. The config payload now exposes resolved userName, userEmail, and userId values for {user-name}, {user-email}, and {user-id} header templates. Browser customization no longer overrides Program.cs profiles; it creates separate browser-local copies so proxied server profiles remain authoritative.
Make Bold Solutions brand alignment: API Test Spark now presents as a Make Bold Spark product across the embedded React UI, favicon set, package icon, NuGet metadata, package README, and public documentation. The app uses Make Bold Solutions colors, logo assets, and Inter Tight typography. No public .NET 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, name, description, base URL, OpenAPI URL, credentials, and headers. Server-configured secrets are redacted from /api-test-spark/config and used only by the server-side profile proxy; browser-created profiles stay local and fetch OpenAPI documents directly. Duplicate profile names are blocked before save. Legacy single-remote options remain supported as a compatibility seed.
Remote API Explorer: browse and test remote REST APIs from OpenAPI documents. Configure one or more RemoteApiProfiles in Program.cs, or use the legacy RemoteBaseUrl and RemoteOpenApiUrl single-remote options as a compatibility profile. The server-side proxy fetches server-configured specs by profile id with credentials injected server-side and redacted from config. Header token expansion: {session-guid} and {request-guid} are expanded in header values at request time. Harness version and build date now shown on the About page. HeadersEditor focus-loss fix.
Response panel developer-experience improvements: editable depth-1 nested object sub-forms (collapsed by default, edited values merge into "Copy as JSON"), "Copy as cURL" button in the response panel, pretty/minified JSON toggle with session-persistent preference, JSONPath tooltips on every field label (click to copy), and 2-row table truncation with show-all/show-less. buildCurl extracted to a shared src/utils/curlBuilder.ts utility. No .NET public API changes.
New EnableDemoIntegrations option — set to false to hide the built-in JokeAPI and JSONPlaceholder demo screens and present only your host API and the API Doc Builder. TypeScript type system hardened: ErrorCategory union expanded with 'React'; ErrorRecord.category and ErrorResponse.category now typed as ErrorCategory (was string). ErrorBoundary observability corrected to use category: 'React' and route exclusively through addError. Constitution amended to v1.1.1.
Fixed Content-Security-Policy on the index.html SPA fallback to allow localhost WebSocket and HTTP connections in Development, so ASP.NET Core Browser Link and hot-reload no longer block the harness from loading. No public API changes.
Documentation update — all docs brought current to code.
Initial release. OpenAPI autodiscovery, collapsible accordion endpoint groups, full response documentation, API Doc Builder, debug panel, Azure Application Insights integration, environment gating, curl generation.
See CHANGELOG.md for the full history.
ApiTestSpark/ # .NET NuGet library — MapApiTestSpark() extension
ApiTestSpark.Tests/ # .NET MSTest integration tests
SampleApi/ # Demo + product site (live at apitest.makeboldspark.com)
src/ # React 19 / TypeScript SPA source
scripts/ # PowerShell build, lint, and pack helpers
.documentation/ # Feature specs, plan, constitution
- React 19 / TypeScript 5.x / Vite 8
- Zustand 5 (persist) — config, auth, harness, debug stores
- TanStack Query 5 — mutation-based API calls
- Tailwind CSS 4 — utility-first styles
- React Router DOM 7 — client-side routing
- .NET 10 / ASP.NET Core Minimal API
EmbeddedFileProviderfor zero-copy static asset servingMicrosoft.SourceLink.GitHub— step-through debugging supportMicrosoft.CodeAnalysis.PublicApiAnalyzers— public API surface tracking
.\scripts\build\dev.ps1 # Start React dev server (http://localhost:5151)
.\scripts\build\build.ps1 # Production build (tsc -b + vite)
.\scripts\build\pack.ps1 # Build SPA + pack NuGet
.\scripts\lint\lint.ps1 # ESLint check
.\scripts\lint\fix.ps1 # Auto-fix linting
dotnet build ApiTestSpark # Build .NET library
dotnet test ApiTestSpark.Tests # Run integration testsQuality gates (required before merge):
npm run lint— zero ESLint errorsnpm run verify—tsc -b+vite buildwith zero errorsdotnet build ApiTestSpark— zero C# errorsdotnet test ApiTestSpark.Tests— all tests pass
- The
/api-test-spark/configendpoint is publicly accessible and returns metadata only — auth scheme name and header names, never token values. - Use
options.Environments = ["Development", "Staging"]to prevent the harness from loading in production. - API tokens stored in the browser are base64-obfuscated in
localStorage— treat the browser as an untrusted environment.
Does it work on .NET 8 or 9?
The package targets net10.0. For .NET 8/9, reference the source directly and adjust the target framework. For .NET 8 you will also need Swashbuckle for OpenAPI — point OpenApiUrl at the Swagger JSON URL.
Will it conflict with my existing middleware?
No. The harness is scoped entirely to /api-test-spark/. It does not modify any other routes, middleware, or pipeline behaviour.
Can I use it behind a reverse proxy?
Yes. Call app.UseForwardedHeaders() before app.MapApiTestSpark() so the config endpoint reports the correct public base URL.
Can I hide the JokeAPI and JSONPlaceholder demo screens?
Yes. Set options.EnableDemoIntegrations = false. 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.
Does it support OpenAPI v2 / Swagger 2.0?
Only OpenAPI v3.x. Use converter.swagger.io to produce a v3 document and point OpenApiUrl at it.
API Test Spark's only input is your OpenAPI v3 document. Everything it renders — endpoint groups, descriptions, request scaffolds, response schemas, status-code badges — comes directly from that document. The richer your OpenAPI metadata, the better your test harness.
| OpenAPI feature | What API Test Spark does with it | Impact |
|---|---|---|
tags in "Namespace: Label" format |
Two-level collapsible accordion groups | High |
summary on each operation |
Bold title on every endpoint card | High |
description (markdown) on each operation |
Rendered below the summary — bold, lists, code blocks, tables | High |
operationId / WithName() |
Copyable chip beside each endpoint; API Doc Builder section heading | High |
Request body schema with example / default |
JSON scaffold pre-filled in the request body editor | High |
Schema property description |
Shown in the property table beside each field | High |
Produces<T> per status code |
Coloured badges with expandable inline schemas | High |
info.title, info.version, info.contact |
API info header at 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 parameter table; example pre-fills path/query fields | Medium |
Schema constraints (minLength, maximum, enum) |
Constraint columns in property tables; enum drives a select input | Medium |
deprecated: true |
Endpoint visually flagged in the accordion | Low |
1. Tag with "Namespace: Label" format
// Minimal API — on the group
app.MapGroup("/products").WithTags("Products: Catalog").MapProducts();
// Controller
[Tags("Products: Catalog")]
public class ProductsController : ControllerBase { }The colon-space separator creates a two-level accordion. Without it, all endpoints land in one unsorted list.
2. Give every operation a summary, description, and name
group.MapGet("/{id}", GetById)
.WithName("GetProductById") // operationId → copyable chip
.WithSummary("Get a product by ID") // card title
.WithDescription(
"Returns a single product. Seeded IDs are **1–10**. " +
"Returns **404** if no product exists with the given ID.");Descriptions accept markdown. Use bold for key values, inline code for field names, and numbered lists for workflow steps.
3. Declare every response code with Produces<T>
group.MapGet("/{id}", GetById)
.Produces<Product>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);Or use TypedResults — it infers response types automatically:
static Results<Ok<Product>, NotFound> GetById(int id, ProductCache cache) =>
cache.GetById(id) is { } p ? TypedResults.Ok(p) : TypedResults.NotFound();Each declared status code becomes a coloured badge. Undeclared codes produce no badge and no inline schema.
4. Annotate schema types with descriptions and constraints
public record Product(
[property: Description("Server-assigned unique identifier. Ignored on create.")]
int Id,
[property: Description("Display name shown in the catalog.")]
[property: Required][property: MinLength(1)][property: MaxLength(100)]
string Name,
[property: Description("Unit price in USD.")]
[property: Range(0.01, 99999.99)]
decimal Price
);[Description], [Required], [Range], [MinLength], [MaxLength] all appear as columns in the schema property table.
5. Set examples and defaults to pre-fill the JSON scaffold
The scaffold fills from example → default → enum[0] → type placeholder. Without examples, every field shows a generic placeholder:
// Record default parameter values
int StockQuantity = 0
// DefaultValue attribute
[DefaultValue("Electronics")]
string? Category
// Full example body via transformer
.WithOpenApi(op => {
op.RequestBody.Content["application/json"].Example =
new OpenApiString("""{"name":"Widget","price":9.99}""");
return op;
})6. Write a workflow walkthrough in info.description
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((doc, _, _) =>
{
doc.Info.Description = """
## Full workflow demo
1. `POST /customers` — create a customer, note the returned `id`
2. `POST /products` — create a product, note the returned `id`
3. `POST /orders` — place an order using those ids
4. `PATCH /orders/{id}/status?status=Confirmed` — advance the order
""";
return Task.CompletedTask;
});
});This renders as markdown in the API info header at the top of the Host API screen.
What degrades the experience
| Pattern | Effect |
|---|---|
No tags |
All endpoints in one unsorted flat list |
No summary |
Cards show raw METHOD /path only |
No Produces<T> |
No response-code badges, no inline schemas |
No property [Description] |
Schema table has blank description column |
IResult return type without TypedResults |
Response schema lost entirely |
| Anonymous object response types | Schema inferred as empty object |
The SampleApi source is the live reference implementation — every practice above is demonstrated there.
| Document | What it covers |
|---|---|
| Current State of OpenAPI in .NET | Complete ecosystem guide — package versions, compatibility matrix, known issues, recommended combinations, React SPA parsing tips |
| NuGet Package Walkthrough | How ApiTestSpark is built and packed — MSBuild/Vite bridge, embedded resources, Source Link, public API tracking, CI/CD |
| Package README | Consumer-facing reference — all ApiTestSparkOptions, quickstart snippets, semver policy |
| Repo Story (2026-06-06) | Evidence-based narrative of this repository's development history, contributor patterns, and architecture — updated with the v1.5.0 release state |
| Live Demo | Running SampleApi on .NET 10 with 16 endpoints and ApiTestSpark installed |
| NuGet Package | Package page, download stats, version history |
ApiTestSpark works with any OpenAPI v3 generator. The recommended stack for a new .NET 10 project:
<!-- Document generation — Microsoft first-party -->
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.8" />
<!-- UI renderer — modern Swagger UI alternative, zero dependencies -->
<PackageReference Include="Scalar.AspNetCore" Version="2.14.14" />builder.Services.AddOpenApi();
var app = builder.Build();
app.MapOpenApi(); // /openapi/v1.json
app.MapScalarApiReference(); // /scalar/v1
app.MapApiTestSpark(); // /api-test-spark/See OPENAPI-DOTNET.md for the full breakdown including Swashbuckle migration, NSwag, build-time generation, and React SPA parsing guidance.
Issues and pull requests are welcome at github.com/MarkHazleton/ApiTestSpark.
MIT — see LICENSE for details.
Built by Mark Hazleton · Part of the Make Bold Spark portfolio · Make Bold Solutions