Skip to main content

How to add an API endpoint

Add a new API endpoint end-to-end: Protobuf schema → Kotlin handler → TypeScript client → tests.

Prerequisites

Steps

Step 1: Define the Protobuf schema

All API contracts live in proto/. Add your message types and update the relevant .proto file.

// proto/pipeline.proto — add new messages
message GetTestRunRequest {
string task_id = 1;
}

message GetTestRunResponse {
TestRunResult result = 1;
string status = 2;
}
caution

Follow backward-compatible rules: never change existing field numbers, only add new fields. See How to add a proto schema.

Step 2: Generate code from proto

# From repo root
bazel build //proto:all

# Generated code appears in schemas/generated/
# NEVER edit files in schemas/generated/ directly

Step 3: Create the Kotlin handler

Create or update a route handler in backend/platform/:

// backend/platform/src/main/kotlin/com/aucert/platform/api/TestRunRoutes.kt
fun Route.testRunRoutes() {
route("/api/v1/test-runs") {
get("/{taskId}") {
val taskId = call.parameters["taskId"]
?: return@get call.respond(HttpStatusCode.BadRequest, "Missing taskId")

val result = testRunService.getResult(taskId)
call.respond(result)
}
}
}
tip

Follow the interface + adapter pattern. The route handler should call a service interface, not concrete implementations. This enables switching between Local (monolith) and Remote (microservice) mode via config flag.

Step 4: Register the route

// In the main application module
fun Application.configureRouting() {
routing {
// ... existing routes
testRunRoutes()
}
}

Step 5: Write backend tests

class TestRunRoutesTest {
@Test
fun `GET test-run returns result`() = testApplication {
// Set up test data
val taskId = "test-123"

client.get("/api/v1/test-runs/$taskId").apply {
assertEquals(HttpStatusCode.OK, status)
val body = body<TestRunResult>()
assertEquals(taskId, body.taskId)
}
}
}
cd backend/platform
./gradlew test --tests "*.TestRunRoutesTest"

Step 6: Add the TypeScript client

// frontend/apps/console/src/api/test-runs.ts
export async function getTestRun(taskId: string): Promise<TestRunResult> {
const response = await fetch(`/api/v1/test-runs/${taskId}`);
if (!response.ok) throw new Error(`Failed: ${response.status}`);
return response.json();
}

Step 7: Write frontend tests

// frontend/apps/console/src/api/__tests__/test-runs.test.ts
describe('getTestRun', () => {
it('fetches test run by task ID', async () => {
// mock fetch or use MSW
const result = await getTestRun('test-123');
expect(result.taskId).toBe('test-123');
});
});
cd frontend/apps/console
pnpm test -- --testPathPattern="test-runs"

Step 8: Verify end-to-end

  1. Start the backend: cd backend/platform && ./gradlew bootRun
  2. Start the frontend: cd frontend/apps/console && pnpm dev
  3. Test the API directly: curl http://localhost:8080/api/v1/test-runs/test-123
  4. Verify the frontend displays the data

Checklist

  • Proto schema updated in proto/
  • Generated code rebuilt (bazel build //proto:all)
  • Kotlin handler uses interface + adapter pattern
  • Backend tests pass
  • TypeScript client added
  • Frontend tests pass
  • End-to-end verification complete
  • Context files updated if architecture changed

What's next