What is an API & How to design it?
In this discussion, I will cover what an API is, what makes a good API, and how to design one effectively. By the end, you’ll have the knowledge to become a better software engineer when it comes to API design.

Introduction to APIs
An API (Application Programming Interface) is a documented way for external consumers (such as other developers, services, or applications) to interact with our code or service without needing to understand its internal workings.
📌 Think of APIs as a contract: you expose what others can call, not how it’s implemented.
An API serves as a contract between the service provider and the consumer, specifying:
- What functionality is available
- How to request that functionality
- What responses to expect
- What errors might occur
API Example — Get Admins of a WhatsApp Group
Imagine a user wants to retrieve all admins of a WhatsApp group. WhatsApp might expose a function like:
function getAdmins(string groupID)
Example API Call:
GET /group/getAdmins
{
"groupId": "xyz123"
}
API Components:
- Function Name:
getAdmins
- Input:
groupID
(string) - Output:
List<Admin>
objects (each containing properties like name, user ID, etc.)
✅ Success Response: A list of
Admin
objects (typically returned as JSON)
Possible Errors:
- GroupDoesNotExist
- GroupDeleted
(may or may not be treated as an error depending on business logic)
📌 The
Admin
object may be complex (e.g., containing name, userID, role, etc.) and is typically returned in JSON format.

API Design Fundamentals
The useful checklist that we need to run through while designing a good API are as follows:
1. Purpose & Belonging
- Ask: Where does this function/API belong or where should it be exposed?
So if we have a microservice architecture and we have a service as a GroupService, which handles everything related to groups, this is the place where ourgetAdmins
API belongs to, because it is something to do with finding all the admins with particulargroupID
- APIs should be exposed by the service that owns the data/logic.
2. Proper Naming and Scope
Key Principle: The API name should exactly match its functionality(Name functions by what they do).
- Good:
GetAdmins
should only return admins. - Bad: If it returns admin information plus their groups they belongs to, then in that case the API name is misleading or named wrongly. It is no longer
getAdmins
rather it becamegetAdminsAndGroups
something similar. :) ⇒ < NAMING IS IMPORTANT >
🧠 Principle:
Name = action + object + clarity
Common Mistakes:
- Overloading functions with extra functionality not implied by the name
- Making APIs too generic (trying to handle too many use cases)
3. Parameter Design
Principles:
- Only include parameters absolutely necessary for the core functionality and reflect the intent of the action. So that your API is not overloaded.
For example, in our previous example we are asking for groupID
, may be the caller has more than that, may be he is also wants to check if a list of users with their userID
are admins. So in these cases should we also allow the function to accept an additional parameter or not, that depends on the use cases or the context in which we are using the API but the API/function in that case will be better to call it as checkAdmins
, not getAdmins
. :) ⇒ < AGAIN NAMING IS IMPORTANT >
- Avoid optional parameters unless they serve a clear optimization purpose
- Each additional parameter increases complexity and maintenance burden
Optimization Exception(cases when extra parameters are acceptable): When performance is critical (e.g., reducing microservice calls), you might accept additional parameters to avoid subsequent lookups or reduce internal service calls by giving more input upfront. Basically it is like giving your service more information so that it does not to depend on other services for that informations in GroupServices
. In such cases also try to:
- Rename the API to reflect the expanded functionality
- Document the performance benefits clearly
4. Response Design — Keep It Minimal & Relevant
Anti-Patterns:
- Do NOT overload responses with unnecessary data and return it to the caller with more data than requested “just in case” it’s needed later and you don’t need to change the response object.
- Creating overly complex response objects to “future-proof” the API
- Large payloads increase network usage and confuse clients.
Good Practices:
- Return only what’s needed for the current use case
- Keep responses focused and clear
- If more data is needed later, create new specific endpoints
✅ Keep responses concise, structured, and aligned with what the caller expects.
5. Error Handling — Be Clear and Responsible
So there might be cases where people may define all the errors possible on earth for their API and there can also be case where people design absolutely nothing, taking no responsibility and just return a generic response for all the cases that may happen. < though you may tend to define all the possible error for your respective service >
For example:
- If
groupId
is expected to be an integer → enforce the data type to be an integer in the parameter itself, no need for extra error checks for parsing. - In case if
groupID
string is too long and your database query fails because you have defined thegroupID
to be of certain length. So in that case you may not require to return an error that the string is too long rather you can just fail that query. <Although it is subjective in nature, but extremes are always odd> - Define clear, expected errors:
- Group not found
- Group is deleted - Avoid vague or overly generic errors like just 400 or 500.
📌 Don’t go to extremes:
» No errors defined → frustrating
» Every small case defined → over-engineering
» So in general while defining errors, think of the common expectations and the responsibilities that your API is going to have.

HTTP API Design Considerations
1. Routing and Endpoint Structure
A lot of time we may have to expose our APIs on HTTP endpoint and in those cases we need to understand how HTTP request and response object actually works.
Best Practices:
- Place APIs in logical services (e.g., group-related APIs in a Groups service)
- Use clear, hierarchical paths:
/groups/v1/getAdmins
- Version your APIs (e.g.,
v1
) to allow for future changes
For Example:
POST www.exmple.com/group/v1/getAdmins
{
"groupId": "xyz123"
}
Routing Breakdown: The WHERE
question is answered here
www.exmple.com
→ Address of your site/group
→ service, the thing that actually contains the API actions/v1
→ versioning (good for future changes)/getAdmins
→ action or the function that needs to be called.- Request body: JSON
{ groupId: ... }
- Response: JSON array of Admins with IDs and names
const data = {
"admin": [
{ "id": "1", "name": "Admin1" },
{ "id": "2", "name": "Admin2" },
{ "id": "3", "name": "Admin3" }
]
};
GET vs POST
The above we did using POST
method but
- You can also use GET here by passing
groupId=123
in thegetAdmins
function and we can also makegetAdmins
toadmins
:
-GET /group/v1/admins?groupId=123
- Slightly shorter as no request body or payload to be sent along with - Use POST if:
- Payload is large
- Sensitive data (not visible in URL)
- Complex filtering logic
Common Mistakes:
- Avoid mixing routing and action inside payloads (e.g., sending “action”: “getAdmins” in request body). It leads to confusion and poor design.
const request = {
action: "getAdmins",
groupID: "xyz123"
};
// Possible to do this also but it is going to be bad practice.
- Redundancy with HTTP methods i.e. if you’re using HTTP GET, then don’t also call your API as
getAdmins
. The verb is already implied by HTTP. (e.g.,GET /getAdmins
is redundant - better asGET /admins
)
✅ RESTful APIs should be resource-based, not action-based.
2. HTTP Methods
- Use
GET
when you're retrieving information and all parameters are in the URL - Use
POST
when sending data in the request body - Match the HTTP method to the operation’s semantics (GET for read, POST for create, PUT for update, etc.)

Advanced API Design Considerations
1. Side Effects and Atomicity
Atomicity means that operations must complete or fail as a whole unit.
Problem: APIs that perform multiple operations unexpectedly
Example: So suppose that we have an API setAdmins
which takes 2 parameters groupID
as string and admin
as a list of admins who needs to made admins in the groupID
.
POST /group/setAdmins
{
"groupId": "xyz123",
"admins": [...]
}
What Can Go Wrong:
What if these users mentioned in the admins
list are not members of the group?
- In general, we can throw an error stating that since they are not members, they can’t be made the group administrative.
- However, based on the use cases we can also have the scenario when we need to add the non-member to the group and then make him admin.
What if the group doesn’t exist?
- In this case, we need to create the group and then we can add the mentioned members to group and make the admin. This can also be a possible expectation although it is no longer going to be a good API but it is possible.
- First: create group
- Then: add members
- Finally: set admins
🤯 Too many side effects = confusion(we are saying
setAdmins
but we are doing much more than that), hard to test/debug, unclear behavior
Solutions:
- Keep APIs focused on single responsibilities with NO SIDE EFFECTS
- If sometimes you need atomic operations (e.g., create + set admins in one go):
- Create new composite APIs with clear names reflecting all operations
- Or require clients to make multiple calls in sequence - Still, document all side effects well and avoid “magic behavior” hidden behind generic names.
🎯 APIs should do one thing, and do it well. Keep behavior predictable and atomic.
2. Handling Large Responses
Think of a function from which we are getting all the members of a group i.e. almost 200 members and each one of them having their profile which might be pretty detailed. So, effectively what happened is that the response is really large. Therefore one of the things that we can think of is to break the response into pieces.
Two Main Approaches:
1. Pagination:- Responsibility To The Client
- Client requests data in chunks (e.g., 10 records at a time)
- API exposes
offset
andlimit
or uses cursor-based navigation
GET /group/members?groupId=xyz123&offset=0&limit=10
- Maintains statelessness
2. Fragmentation:
- Server breaks response into multiple packets
- Each packet is numbered sequentially
- Final packet signals completion. Include an “end” marker to indicate completion.
- Useful for internal service-to-service communication or Internal technique for server-to-server calls
📦 Helps maintain performance, especially in microservice communication.
3. Consistency vs. Performance
So this for the scenario where we have to choose that how consistent we want our data to be.
Real-World Challenge:
- Get admins for a group
- You read list of admins from DB
- While reading, someone else adds a new admin
- You return stale data
Do You Care?
- Depends on the use case:
» If eventual consistency is acceptable → serve from cache
» If strict consistency required → read from DB directly
Trade-offs:
- Strong consistency: Guarantees up-to-date data but is slower
- Eventual consistency: May return slightly stale data but is faster
Implementation Options:
- Caching: Improves performance but risks stale data
- Service degradation: Under heavy load
» Use cache where possible
» Trim down response: return essential data only, Instead of full profile, just send names
» This is known as graceful degradation - Let clients choose consistency level via parameters

Conclusion
Good API design is about creating clear, focused contracts between services. Key principles include:
- Precise naming that matches functionality
- Minimal, well-defined parameters and responses
- Proper error handling
- Careful consideration of side effects
- Appropriate handling of performance and consistency trade-offs
Well-designed APIs are easier to use, maintain, and evolve over time, making them critical for building scalable, maintainable systems.
🗣️ APIs are like doors to your service. A good door is easy to open, labeled clearly, only lets in who’s allowed, and doesn’t lead to unintended places.
