DEV Community

Shoumik Chakravarty
Shoumik Chakravarty

Posted on

Make API registration a side effect of a successful build

At some point every platform team makes the same mistake: they build a beautiful API marketplace, write up the registration process, and watch developers ignore it completely.

The registration UI has too many steps. Filing a ticket to get an API listed is a two-day wait. The portal docs are stale because nobody remembered to update them when the spec changed. Within six months, half the registered APIs are outdated, and the other half were never registered in the first place.

The root cause isn't laziness. It's friction. If publication requires a human to do something, publication won't happen consistently.

The fix is to make it automatic. Publication should be a side effect of a successful build — something that happens whether or not the developer thinks about it.

Here's how that pipeline works.


The four steps

The pipeline has one job: take what's already in the repo and push it to the registry and the gateway. No tickets. No platform team in the loop for routine updates.

push to main
  → lint the OpenAPI spec
  → check for breaking changes
  → publish to the registry
  → registry propagates to the gateway
Enter fullscreen mode Exit fullscreen mode

That's it. Each step is a gate. If linting fails, the build fails. If there's an undeclared breaking change, the build fails. The spec only lands in the marketplace if it passes.


The GitHub Actions workflow

name: publish-api
on:
  push:
    branches: [main]

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Lint OpenAPI spec
        run: npx @stoplight/spectral-cli lint openapi.yaml

      - name: Check for breaking changes
        run: npx oasdiff breaking https://registry.internal/specs/${{ github.event.repository.name }}/latest openapi.yaml

      - name: Publish to registry
        env:
          REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
        run: |
          curl -X POST https://registry.internal/api/v1/services \
            -H "Authorization: Bearer $REGISTRY_TOKEN" \
            -H "Content-Type: application/json" \
            --data-binary @openapi.yaml
Enter fullscreen mode Exit fullscreen mode

A few things worth calling out.

The lint step uses Spectral. Spectral lets you define custom rulesets — so beyond the standard OpenAPI schema checks, you can enforce your own conventions. Endpoint naming patterns, required fields in every operation, response schema rules. This is where your internal standards get automated rather than just documented.

The breaking-change check uses oasdiff. It pulls the currently-published spec from your internal registry and diffs it against what's in the PR. Removed fields, changed response types, renamed parameters — any of those fail the build. Teams have to either increment the version or go through a deprecation window. This is the single highest-ROI automation in the whole pipeline. A breaking change that ships unannounced is the fastest way to lose every team's trust in the marketplace.

The publish step is just a POST. The workflow doesn't talk to APIM directly. It hits a thin internal API — what we call the Common API Management layer — which handles validation, writes to the registry, and pushes to the gateway. The CI job doesn't need to know about any of that. It hands off the spec and gets a success or failure back.


The publication endpoint

The layer that receives the POST does a few things:

  1. Validates the registration payload — confirms required metadata fields are present (owner team, business domain, criticality, SLO)
  2. Rejects submissions that violate internal naming conventions or have missing ownership fields
  3. Writes the registration to the central registry (we use Cosmos DB for metadata, Blob Storage for the spec itself)
  4. Pushes to Azure API Management via the APIM REST API

Here's a minimal version of the Azure Function that handles this:

[Function("PublishApi")]
public async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = "services")] HttpRequest req,
    ILogger log)
{
    var body = await new StreamReader(req.Body).ReadToEndAsync();
    var registration = JsonSerializer.Deserialize<ApiRegistration>(body);

    if (registration is null || string.IsNullOrEmpty(registration.OpenApiSpec))
        return new BadRequestObjectResult("Missing or invalid registration payload.");

    var violations = _validator.Validate(registration);
    if (violations.Any())
        return new UnprocessableEntityObjectResult(violations);

    await _registryClient.UpsertAsync(registration);

    await _apimClient.ImportOrUpdateApiAsync(
        apiId: registration.ServiceName,
        spec: registration.OpenApiSpec,
        subscriptionKey: _config["Apim:SubscriptionKey"]);

    log.LogInformation("Published {ServiceName} v{Version} to registry and gateway.",
        registration.ServiceName, registration.Version);

    return new OkObjectResult(new { status = "published", id = registration.ServiceName });
}
Enter fullscreen mode Exit fullscreen mode

The ApiRegistration model carries the OpenAPI spec alongside mandatory metadata — service name, version, owner team, business domain, criticality. The validator runs before anything touches the registry or gateway. Bad submissions get rejected at the API boundary, not discovered later when someone notices the marketplace entry is wrong.


Treat the spec like code

The thing that makes this pipeline actually work long-term is treating openapi.yaml the same way you'd treat main.go or Program.cs. It lives in the repo. It gets reviewed in PRs. It fails the build when it breaks conventions.

Most teams start by dropping the spec into a separate docs repo, or generating it as a CI artifact that gets published independently. Both approaches create drift. The spec and the implementation diverge, and developers stop trusting the marketplace because the docs don't match what the service actually does.

Keep the spec in the service repo. Version it with the code. Let the CI pipeline be the thing that enforces consistency — not a style guide on Confluence that nobody reads after the first week.


What breaks first

Running this in production with hundreds of services teaches you a few things quickly.

The breaking-change check will catch things you didn't expect to be breaking. Renaming a field in a shared response model that's used across six endpoints counts as six breaking changes. Teams push back on this at first. Then they ship a breaking change in a rush once, break two downstream consumers, and never push back again.

Secrets in specs are more common than you'd think. Someone pastes an example response that has a real access token in it. Someone else hardcodes an internal hostname. The continuous scan step should include a secrets-in-spec check — most open-source API linting tools support this with a plugin or custom rule.

First-time registration is different from an update. The pipeline needs to know whether this spec is brand new or a revision of something already in the registry. Handle this at the publication layer — check for an existing entry, treat the call as an upsert, and tag new registrations for a one-time human review before they go live. Updates from there can be fully automated.


The payoff

Once this is running, the developer's job is simple: keep openapi.yaml in sync with the implementation, push to main, done. The API lands in the marketplace automatically. The registry is current. The gateway is updated. No tickets.

The platform team shifts from being the bottleneck to being the people who maintain the pipeline. That's the right trade.


Shoumik Chakravarty is an Associate Director of Software Engineering at Verizon, where he designs and operates large-scale distributed systems and API platforms serving tens of millions of users. Find him on dev.to and LinkedIn.

Top comments (0)