Skip to content

Browsable API Interface

Surkyl Server provides a self-documenting, interactive API browser interface. When users access API endpoints through a web browser, they receive an HTML interface with API documentation and interactive testing capabilities. Standard API clients continue to receive JSON/raw responses automatically.

The browsable API system provides:

  • Automatic Content Negotiation - Detects browser vs API client requests
  • Interactive Testing Interface - Test APIs directly from the browser
  • OpenAPI Integration - Pulls metadata from your OpenAPI specification
  • Zero Breaking Changes - Existing API clients work unchanged
  • Surkyl Branding - Dark theme with Surkyl brand colors

When you visit any API endpoint in a web browser:

  • Root URL (/) → Shows the API index with all available endpoints
  • Any endpoint (/status, /health) → Opens the interactive testing interface
  • Manual override → Use ?format=html to force HTML view

Standard API clients (curl, etc.) automatically receive JSON:

Terminal window
# Returns JSON response
curl http://localhost:64001/status
# Force JSON even in browser
curl http://localhost:64001/status?format=json
  1. Query Parameter (highest priority): ?format=json|html
  2. Accept Header: Accept: text/html vs Accept: application/json
  3. Default: JSON for API clients

The API testing interface includes:

  • URL Bar - Method selector and editable URL
  • Request Builder - Tabs for Params, Headers, Body, and Info
  • Query Parameters - Add/remove key-value pairs with checkboxes
  • Headers Editor - Customize request headers
  • Request Body - JSON editor for POST/PUT/PATCH requests
  • Response Viewer - Syntax-highlighted JSON with copy functionality
  • Copy as cURL - Export request as a cURL command

The root page (/) displays:

  • Server name and version (when show_version is enabled)
  • Total number of endpoints
  • Quick links to OpenAPI specs
  • Grid of all available endpoints with:
    • HTTP method badges (GET, POST, PUT, DELETE, PATCH)
    • Endpoint path
    • Summary description
    • Tags/categories

Control version display via server settings:

configs/surkyl-server.config.yml
server:
show_version: true # Set to false to hide version in UI

When show_version is false:

  • Version number is hidden in the UI header
  • Version stat card is removed from root page
  • JSON responses respect this setting as well

Developer Guide: Making Handlers Compliant

Section titled “Developer Guide: Making Handlers Compliant”

To ensure your API handlers work with the browsable API system, follow these guidelines:

Every handler that should support browsable API must accept the NegotiatedFormat extractor:

use crate::browsable::NegotiatedFormat;
pub async fn my_handler(
// ... other extractors
format: NegotiatedFormat, // Add this parameter
) -> impl IntoResponse {
// handler logic
}

Instead of returning Json<T>, wrap your response with BrowsableResponse:

use crate::browsable::BrowsableResponse;
pub async fn status_handler(
server_settings: Extension<Arc<ServerSettings>>,
format: NegotiatedFormat,
) -> impl IntoResponse {
let response = StatusResponse {
status: ServerStatus::Alive,
message: "The server is running!".to_string(),
// ... other fields
};
// Wrap with BrowsableResponse
BrowsableResponse::from_path(response, "/status", "GET", format)
.with_show_version(server_settings.server.show_version)
}

Add your endpoint to src/openapi.yml so it appears in the browsable interface:

paths:
/my-endpoint:
get:
tags:
- MyCategory
summary: Brief description of what this does
description: Detailed explanation of the endpoint functionality
operationId: myEndpointOperation
responses:
'200':
description: Success response
content:
application/json:
schema:
$ref: '#/components/schemas/MyResponseSchema'

Here’s a full example of a compliant handler:

use crate::browsable::{BrowsableResponse, NegotiatedFormat};
use crate::config::server::ServerSettings;
use axum::{Extension, response::IntoResponse};
use std::sync::Arc;
#[derive(serde::Serialize, schemars::JsonSchema)]
pub struct MyResponse {
pub data: String,
pub count: i32,
}
pub async fn my_data_handler(
Extension(server_settings): Extension<Arc<ServerSettings>>,
format: NegotiatedFormat,
) -> impl IntoResponse {
let response = MyResponse {
data: "Hello from API".to_string(),
count: 42,
};
BrowsableResponse::from_path(response, "/my-data", "GET", format)
.with_show_version(server_settings.server.show_version)
}

Your response type must implement Serialize:

use serde::Serialize;
use schemars::JsonSchema;
#[derive(Serialize, JsonSchema)] // Both traits required
pub struct MyResponse {
pub field1: String,
pub field2: Option<i32>,
}

For handlers that return non-JSON (like plain text), check the format preference manually:

pub async fn root_handler(
server_settings: Extension<Arc<ServerSettings>>,
format: NegotiatedFormat,
) -> impl IntoResponse {
if format.wants_html {
return crate::browsable::response::render_api_root(
format,
server_settings.server.show_version,
);
}
// Return plain text for API clients
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "text/plain")
.body(Body::from("Server Name"))
.unwrap()
}
  • Always add NegotiatedFormat extractor to browsable handlers
  • Use BrowsableResponse::from_path() for automatic metadata loading
  • Call .with_show_version() to respect configuration
  • Document endpoints in OpenAPI spec for rich metadata
  • Use Serialize and JsonSchema derives on response types
  • Test both browser and API client access
  • Return raw Json<T> without BrowsableResponse wrapper
  • Forget to add endpoint to openapi.yml
  • Hardcode version strings - use MANIFEST
  • Ignore the show_version setting
src/browsable/
├── mod.rs # Module exports
├── extractor.rs # NegotiatedFormat content negotiation
├── metadata.rs # Endpoint metadata registry (loads from OpenAPI)
└── response.rs # BrowsableResponse wrapper
templates/
├── api_browser.html # Interactive testing interface
└── api_root.html # API index page
  1. Request arrives at handler
  2. NegotiatedFormat extractor parses Accept header and query params
  3. Handler creates response data
  4. BrowsableResponse wrapper checks format.wants_html
  5. If browser: Renders HTML template with response data
  6. If API client: Returns raw JSON

On server startup, the metadata registry loads from openapi.yml:

// In main.rs
let openapi_yaml = include_str!("openapi.yml");
init_registry(openapi_yaml)?;

This provides:

  • Endpoint summaries and descriptions
  • Tags for categorization
  • Request/response schemas
  • Allowed HTTP methods
  • Check if endpoint has NegotiatedFormat extractor
  • Verify handler uses BrowsableResponse wrapper
  • Try explicit ?format=html parameter
  • Ensure endpoint is registered in openapi.yml
  • Verify OpenAPI syntax is valid
  • Check that metadata registry initialized without errors
  • Verify .with_show_version(server_settings.server.show_version) is called
  • Check config file has show_version: false
  • Restart server after config change