How to add an API endpoint
Add a new API endpoint end-to-end: Protobuf schema → Kotlin handler → TypeScript client → tests.
Prerequisites
- Backend running locally (How to run the backend)
protocinstalled (via Bazel or standalone)- Understanding of the 5-layer pipeline
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
- Start the backend:
cd backend/platform && ./gradlew bootRun - Start the frontend:
cd frontend/apps/console && pnpm dev - Test the API directly:
curl http://localhost:8080/api/v1/test-runs/test-123 - 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
- How to add a proto schema — Detailed Protobuf workflow
- How to run tests — Full test suite
- How to deploy to dev — Push to AKS