Posted on 16 Sep 2026

In the previous installments of this series, we explored CLI application patterns with Click and Typer. While CLI interfaces offer a convenient way for developers to interact with services locally, building distributed applications requires exposing services over the network via structured HTTP interfaces.
When designing a REST API, engineers often focus on choosing the “right” framework — FastAPI, Spring Boot, and so on. Tools matter, but they solve only the implementation detail. The real challenge is designing an API that reflects the business domain, exposes domain entities as resources, and uses HTTP semantics consistently, including errors and status codes.
Think of an API as a contract between two systems. As the designer of this contract, you act like a notary, carefully drafting the terms of interaction. However, this contract must follow the rules of HTTP, ensuring it adheres to the standard conventions for communication.
The first step is to identify the entities involved in the contract. Once these entities are clear, the next step is to define the rules: what operations can be performed on each entity, and under what conditions, all while respecting the HTTP framework.
A good API should be intuitive, self-describing, and coherent with the domain. This requires identifying business entities, their lifecycle, and their relationships before thinking about technology. Once the domain model is clear, the API becomes almost a direct consequence of it.
To avoid remaining purely theoretical, we will use Kubernetes as a case study. It provides a concrete example where functional requirements naturally lead to entities and operations that are directly exposed through an HTTP interface.
You should read this article if:
In a Clean Architecture perspective, the design process begins from the inside: entities at the center, then use cases, then the external interfaces.
Clean Architecture Main Concepts
User stories play a fundamental role here. They describe what the system must accomplish from a business perspective, and in doing so they implicitly define the domain language: the nouns in the stories become the entities of the domain, while the verbs describe the operations and relationships between them.
Kubernetes is no exception. Its functional requirements — scheduling workloads, distributing containers, maintaining stable network access, scaling services, enforcing security boundaries — naturally give rise to a set of domain entities. These entities exist independently of any API design choice; the API merely exposes them.
In this section, I assume the requirements of Kubernetes are already known and focus directly on their consequences: the entities that form the system and the relationships between them.
Kubernetes Entities with their Relationships
In Kubernetes everything is a Resource. Conceptually, Kubernetes exposes two main categories of resources:
Although the “cluster” itself is not a first-class API object, we can treat it as the top-level element for reasoning about relationships.
Pods are ephemeral and their network identity is unstable. For this reason, Pods are typically exposed through a Service (namespaced resource). Services come in three main variants:
Workloads are created and managed by Workload (this concept doesn’t explicitly exist in Kubernetes but it helps us to model the resources). The most common ones are:
Pods may rely on persistent storage. They reference one or more PersistentVolumeClaims (PVCs), each of which binds to a PersistentVolume (PV) in a one-to-one manner.
Finally, users are not represented by Kubernetes YAML objects but are defined in the Kubernetes configuration (commonly via the kubeconfig file). Users acquire permissions through Roles (namespaced) and ClusterRoles (cluster-scoped). Roles are connected to users through RoleBindings or ClusterRoleBindings, depending on the scope.
Once this entity model is clear, designing a RESTful API becomes almost mechanical. Each entity maps directly to a resource, operations correspond to the entity’s lifecycle, and access control follows from the relationships defined in the domain model.
Once we have identified the entities in the system, the next step is to define the rules of interaction between these entities. These rules will dictate how resources can be accessed, manipulated, or deleted via the API. Each rule can be broken down into four main components:
Defining the Rules of an API
The endpoint is the URL pattern that maps to the entity and defines the resource’s location. It is the path that the client will use to access the resource or perform an action on it.
A few important principles:
Use plural names for entities:
Even when retrieving a single entity, the resource name in the endpoint should be plural to denote the collection.
Example:
/api/v1/namespaces/{namespace}/pods # collection of pods
/api/v1/namespaces/{namespace}/pods/{pod_id} # single pod
Using the plural form consistently improves readability, makes endpoints predictable, and clearly distinguishes between collections and individual entities.
Limit relationship depth to two entities:
When representing relationships between entities in the endpoint, it is recommended to limit the path to two entities (e.g., /api/v1/customers/{customer_id}/employees). Going beyond this introduces unnecessary complexity and can lead to performance issues, as deeper relationships make the endpoint more difficult to manage and can impact scalability. Keeping the representation simple ensures better performance and maintainability.
The operation specifies the action to be performed on the resource. This is determined by the HTTP method used in the request. The most common HTTP methods include:
Some actions, like restarting a Pod, are not CRUD operations but can be modeled using POST, as they trigger an action on a resource without modifying it directly.
Parameters define the information needed to execute the operation. There are three types of parameters that can be used:
/pods/{pod_id}).?limit=10 or ?status=active).The status code is the HTTP response code that indicates the result of the operation. Common status codes include:
The response is the data returned from the server after processing the request. This can include:
The response should be formatted in a structured and predictable way (typically JSON or XML) so that the client can easily parse and understand the result.
These components — endpoint, operation, parameters, status code, and response — form the backbone of any API rule. By clearly defining these elements, you ensure that the API is intuitive, consistent, and easy to use for clients. These rules are the foundation that will guide how entities interact with each other in a system, ensuring that the API remains predictable, maintainable, and aligned with business needs.
Since it’s impossible in an article cover the API for all the above mentioned resources, let’s see how to define the API for the Pod resource.
In Kubernetes, these are the typical operations you can perform on a Pod, including both CRUD operations and a non-CRUD action like restarting a Pod.
Managing Pod via API
/api/v1/namespaces/{namespace}/pods/api/v1/namespaces/{namespace}/pods (to list all Pods in a namespace) or /api/v1/namespaces/{namespace}/pods/{pod_id} (to retrieve a specific Pod)namespace, pod_id), Query parameters (e.g., ?status=running)/api/v1/namespaces/{namespace}/pods/{pod_id}/api/v1/namespaces/{namespace}/pods/{pod_id}/api/v1/namespaces/{namespace}/pods/{pod_id}namespace, pod_id)In any API, authentication and authorization are critical to ensure that only legitimate users can perform allowed operations on the system’s resources. Conceptually, these principles apply to all APIs, not just Kubernetes.
Authentication and Authorization
Principle: If the identity cannot be verified, the API must return HTTP 401 Unauthorized.
Principle: If the user is authenticated but lacks permission for an operation, the API must return HTTP 403 Forbidden.
When you design your application you need to:
Kubernetes illustrates these concepts:
This design allows a many-to-many mapping: users can have multiple roles, and roles can apply to multiple users.
When a user interacts with the API, the system maintains a context that encapsulates all relevant information:
This context is used by the application to enforce authorization rules consistently for each request.
Kubernetes demonstrates this clean separation of identity (authentication), capability (authorization), and scope (context), providing a model that can be applied to any API design.
One of the key differences between a professional API and one created by inexperienced developers is how errors are handled. Proper error handling ensures that clients can reliably understand what went wrong and respond appropriately.
While frameworks like FastAPI (Python) or Spring Boot (Java) provide built-in mechanisms for error handling, the principles are framework-agnostic.
API Error Handling
Example:
GET /api/v1/namespaces/default/pods/nonexistent
Response: 404 Not Found
If the client sends invalid input, such as:
the API should return 400 Bad Request or 422 Unprocessable Entity.
Frameworks like FastAPI + Pydantic or Spring Boot with validation annotations make it easy to automatically validate input and return the appropriate status code.
These errors were covered in the Authentication and Authorization section, but they are a crucial part of robust error handling.
405 Method Not Allowed is returned when the client attempts to use an HTTP method that is not supported for a given resource.
Example:
POST /api/v1/customers/{id}
Response: 405 Method Not Allowed
This error is typically implemented automatically by the framework (e.g., FastAPI or Spring Boot), based on how routes and HTTP methods are defined. The application logic should not manually handle this case.
429 Too Many Requests indicates that the client has exceeded the allowed request rate.
This error is:
Common use cases include:
The response often includes headers such as Retry-After to indicate when the client can retry.
These codes allow clients to distinguish between a system failure and a temporary unavailability.
APIs should be self-describing, meaning that each endpoint should be clear and understandable without requiring external documentation. However, tools like Swagger have greatly simplified the publication and management of API documentation. Frameworks such as FastAPI allow you to automatically generate Swagger documentation for every API, making it interactive and easily accessible.
API Documentation with Swagger
When using Swagger, it’s a best practice to group APIs by the entities they belong to. In FastAPI, this can be achieved using tags. For example, all APIs related to a specific entity, such as Customer, can be grouped under the Customer tag. This approach helps to keep the documentation clear and well-structured, with each group of APIs representing the “rules” for that particular entity.
For every API, it’s essential to properly document the following details in the code:
These details will automatically be included in the Swagger documentation, making it comprehensive and easy to navigate.
Moreover, Swagger also acts as a client, allowing developers to directly test the APIs from the documentation interface, which simplifies testing and debugging during development.
In this article we covered:
GET, POST, PUT, PATCH, DELETE) with predictable status codes.Role, ClusterRole, RoleBinding) as an architectural blueprint.The next article will put these architectural principles into practice: building a production-ready asynchronous REST API using FastAPI, with thread pools for blocking operations and dependency injection patterns.
If you enjoyed this article, don’t forget to give it a clap 👏, share it with your friends 🔗, and follow me for more tips and tutorials on software development 📘. Your support helps me create more content like this — thank you! 🙌