Getting Started with the Stella Ops Router

Audience: Developers building a microservice that joins the Stella Ops Router fabric. Scope: A hands-on walkthrough that builds a small order service and a gateway end to end, then points you at production hardening (transports, auth, rate limiting, observability).

For the architecture and rationale, see the Router module overview. For admission control, see Rate Limiting.

Prerequisites

Step 1: Create the Microservice Project

mkdir MyService
cd MyService
dotnet new console -n MyService
cd MyService

Add the required packages:

dotnet add package StellaOps.Microservice
dotnet add package StellaOps.Router.Transport.InMemory

Step 2: Define Your Data Models

Create Models/OrderModels.cs:

namespace MyService.Models;

public sealed class CreateOrderRequest
{
    public required string CustomerId { get; init; }
    public required decimal Amount { get; init; }
    public string? Description { get; init; }
}

public sealed class CreateOrderResponse
{
    public required string OrderId { get; init; }
    public DateTimeOffset CreatedAt { get; init; }
    public string Status { get; init; } = "created";
}

public sealed class Order
{
    public required string OrderId { get; init; }
    public required string CustomerId { get; init; }
    public required decimal Amount { get; init; }
    public string? Description { get; init; }
    public DateTimeOffset CreatedAt { get; init; }
}

Step 3: Create Endpoints

Create Endpoints/CreateOrderEndpoint.cs:

using MyService.Models;
using StellaOps.Microservice;

namespace MyService.Endpoints;

/// <summary>
/// Creates a new order.
/// </summary>
[StellaEndpoint("POST", "/orders", TimeoutSeconds = 30)]
public sealed class CreateOrderEndpoint : IStellaEndpoint<CreateOrderRequest, CreateOrderResponse>
{
    private readonly ILogger<CreateOrderEndpoint> _logger;

    public CreateOrderEndpoint(ILogger<CreateOrderEndpoint> logger)
    {
        _logger = logger;
    }

    public Task<CreateOrderResponse> HandleAsync(
        CreateOrderRequest request,
        CancellationToken cancellationToken)
    {
        var orderId = $"ORD-{Guid.NewGuid():N}"[..16];

        _logger.LogInformation(
            "Created order {OrderId} for customer {CustomerId}, amount: {Amount}",
            orderId, request.CustomerId, request.Amount);

        return Task.FromResult(new CreateOrderResponse
        {
            OrderId = orderId,
            CreatedAt = DateTimeOffset.UtcNow
        });
    }
}

Create Endpoints/GetOrderEndpoint.cs:

using MyService.Models;
using StellaOps.Microservice;

namespace MyService.Endpoints;

/// <summary>
/// Retrieves an order by ID.
/// </summary>
[StellaEndpoint("GET", "/orders/{id}", TimeoutSeconds = 10)]
public sealed class GetOrderEndpoint : IRawStellaEndpoint
{
    private readonly ILogger<GetOrderEndpoint> _logger;

    public GetOrderEndpoint(ILogger<GetOrderEndpoint> logger)
    {
        _logger = logger;
    }

    public Task<RawResponse> HandleAsync(
        RawRequestContext context,
        CancellationToken cancellationToken)
    {
        var orderId = context.PathParameters["id"];

        _logger.LogInformation("Fetching order {OrderId}", orderId);

        // In a real app, you'd look up the order from a database
        var order = new Order
        {
            OrderId = orderId,
            CustomerId = "CUST-001",
            Amount = 99.99m,
            Description = "Sample order",
            CreatedAt = DateTimeOffset.UtcNow.AddHours(-1)
        };

        var json = System.Text.Json.JsonSerializer.Serialize(order);
        var body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(json));

        var headers = new HeaderCollection();
        headers.Set("Content-Type", "application/json");

        return Task.FromResult(new RawResponse
        {
            StatusCode = 200,
            Headers = headers,
            Body = body
        });
    }
}

Step 4: Configure the Service

Update Program.cs:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MyService.Endpoints;
using StellaOps.Microservice;
using StellaOps.Router.Common.Enums;
using StellaOps.Router.Transport.InMemory;

var builder = Host.CreateApplicationBuilder(args);

// Configure logging
builder.Logging.AddConsole();
builder.Logging.SetMinimumLevel(LogLevel.Debug);

// Configure the microservice
builder.Services.AddStellaMicroservice(options =>
{
    options.ServiceName = "order-service";
    options.Version = "1.0.0";
    options.Region = "local";
    options.InstanceId = $"order-{Environment.MachineName}";

    // Connect to gateway (use InMemory for development)
    options.Routers =
    [
        new RouterEndpointConfig
        {
            Host = "localhost",
            Port = 5100,
            TransportType = TransportType.InMemory
        }
    ];
});

// Register transport
builder.Services.AddInMemoryTransport();

// Register endpoint handlers
builder.Services.AddScoped<CreateOrderEndpoint>();
builder.Services.AddScoped<GetOrderEndpoint>();

var host = builder.Build();

Console.WriteLine("Order Service starting...");
Console.WriteLine("Endpoints:");
Console.WriteLine("  POST /orders");
Console.WriteLine("  GET  /orders/{id}");

await host.RunAsync();

Step 5: Create the Gateway

Create a separate project for the gateway:

cd ..
dotnet new web -n MyGateway
cd MyGateway
dotnet add package StellaOps.Router.Gateway
dotnet add package StellaOps.Router.Transport.InMemory

Update Program.cs:

using StellaOps.Router.Gateway;
using StellaOps.Router.Gateway.Authorization;
using StellaOps.Router.Gateway.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

// Add gateway services
builder.Services.AddRouterGateway(builder.Configuration);

// Add process-local transport only for an explicit local harness
builder.Services.AddRouterGatewayLocalInMemoryHarness(builder.Environment);

// No-op authorization for demo (use real auth in production)
builder.Services.AddNoOpAuthorityIntegration();
builder.Services.AddAuthentication();

// OpenAPI
builder.Services.AddEndpointsApiExplorer();

var app = builder.Build();

// Middleware pipeline
app.UseAuthentication();
app.UseClaimsAuthorization();

// OpenAPI endpoint (aggregates from all microservices)
app.MapRouterOpenApi();

// Health check
app.MapGet("/health", () => Results.Ok(new { status = "healthy" }));

// Router gateway handles all other routes
app.UseRouterGateway();

Console.WriteLine("Gateway starting on http://localhost:5000");
app.Run("http://localhost:5000");

Step 6: Run the Services

Open two terminals:

Terminal 1 - Gateway:

cd MyGateway
dotnet run

Terminal 2 - Microservice:

cd MyService
dotnet run

Step 7: Test the API

Create an order:

curl -X POST http://localhost:5000/orders \
  -H "Content-Type: application/json" \
  -d '{"customerId": "CUST-001", "amount": 99.99}'

Response:

{
  "orderId": "ORD-abc123def456",
  "createdAt": "2024-01-15T10:30:00Z",
  "status": "created"
}

Get an order:

curl http://localhost:5000/orders/ORD-abc123def456

Step 8: Add Validation (Optional)

Add JSON Schema validation to your endpoints:

using StellaOps.Microservice;
using StellaOps.Microservice.Validation;

[StellaEndpoint("POST", "/orders", TimeoutSeconds = 30)]
[ValidateSchema]  // Enables JSON Schema validation
public sealed class CreateOrderEndpoint : IStellaEndpoint<CreateOrderRequest, CreateOrderResponse>
{
    // ...
}

The source generator will create a JSON Schema based on your request type’s properties and validate incoming requests automatically.

Step 9: Add Streaming Support (Optional)

For large file uploads or real-time data:

[StellaEndpoint("POST", "/orders/{id}/documents", SupportsStreaming = true)]
public sealed class UploadDocumentEndpoint : IRawStellaEndpoint
{
    public async Task<RawResponse> HandleAsync(
        RawRequestContext context,
        CancellationToken cancellationToken)
    {
        var orderId = context.PathParameters["id"];
        var contentType = context.Headers["Content-Type"] ?? "application/octet-stream";

        // Stream the body directly without buffering
        await using var bodyStream = context.Body;
        var totalBytes = 0L;

        var buffer = new byte[8192];
        int bytesRead;
        while ((bytesRead = await bodyStream.ReadAsync(buffer, cancellationToken)) > 0)
        {
            totalBytes += bytesRead;
            // Process chunk...
        }

        var headers = new HeaderCollection();
        headers.Set("Content-Type", "application/json");

        var response = $$"""{"orderId":"{{orderId}}","bytesReceived":{{totalBytes}}}""";
        return new RawResponse
        {
            StatusCode = 200,
            Headers = headers,
            Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(response))
        };
    }
}

Next Steps

See the samples directory for complete, runnable example projects (gateway plus billing and inventory microservices).