Skip to main content

Command Palette

Search for a command to run...

An API Endpoint Is Not the Same as API Design

Good APIs are designed before the controller action, with clear contracts, predictable behavior, security, and production support in mind.

Updated
11 min read
An API Endpoint Is Not the Same as API Design
D
Senior Software Engineer and .NET Technical Lead focused on building reliable enterprise software with .NET, Azure, Clean Architecture, and practical AI integration. I write about ASP.NET Core, API design, software architecture, Azure, enterprise automation, secure identity, observability, and AI-assisted development. This blog is where I document my journey of going deeper as an engineer — not only writing code, but understanding how to design, build, deploy, and improve real-world systems. My goal is to share practical lessons, technical decisions, trade-offs, mistakes, and examples from modern enterprise software development.

Creating an API endpoint is easy.

Designing an API that other developers can trust is harder.

In ASP.NET Core, exposing an endpoint can be very straightforward:

GET /orders/{id}

POST /orders

PUT /orders/{id}

DELETE /orders/{id}

A controller action or minimal API route can be created quickly. The endpoint may accept a request, call a service, return a response, and look complete.

But an endpoint is not the same as API design.

Good API design starts before the controller action.

It starts with understanding the contract, the consumer, the business rules, the failure scenarios, the security boundaries, and the long-term maintainability of the system.

A clean endpoint with poor contracts can still create problems.

A good API should be predictable, secure, consistent, observable, and easy to consume.

An endpoint is only the surface

An endpoint is the visible part of the API.

It is the URL, HTTP method, request body, response body, and status code that another developer interacts with.

But behind that endpoint, there are many design decisions.

For example:

  • What should the request contract look like?

  • What should the response contract expose?

  • Which fields are required?

  • How should validation errors be returned?

  • Which HTTP status codes should be used?

  • How should authentication and authorization be enforced?

  • Should the operation be idempotent?

  • How should pagination, filtering, and sorting work?

  • What happens when a downstream service fails?

  • How will the API remain backward compatible?

  • How will logs and traces help debug production issues?

These questions are part of API design.

If they are ignored, the endpoint may still work, but it may become difficult to consume, difficult to maintain, and difficult to support in production.

Request contracts should be intentional

A request contract should not simply mirror the database table.

This is a common API design mistake.

The database model and the API contract serve different purposes.

A database model is designed for persistence.

An API request model is designed for communication.

For example, an order creation request should include only the data the client is allowed to send. It should not expose internal fields such as database IDs, audit fields, status transitions, calculated values, or system-managed properties.

A good request contract should be:

  • clear

  • minimal

  • validatable

  • stable

  • aligned with the business operation

For example, this is more intentional:

CreateOrderRequest

with fields such as:

  • customerId

  • orderItems

  • deliveryAddress

  • requestedDeliveryDate

Instead of exposing a full internal Order entity directly.

The request should represent the action the client is trying to perform, not the internal shape of the database.

Response contracts should protect the system

Response contracts are just as important as request contracts.

A common mistake is returning internal domain entities or database models directly from an API.

This can create several problems.

It may expose fields that should remain internal.

It may tightly couple clients to the database structure.

It may make future changes harder.

It may accidentally leak sensitive or unnecessary information.

A response contract should expose what the consumer needs, not everything the system knows.

For example, an order response may include:

  • orderId

  • orderNumber

  • status

  • createdAt

  • totalAmount

  • items

But it may not need to expose:

  • internal workflow flags

  • database tracking fields

  • audit metadata

  • internal notes

  • security-related fields

  • implementation-specific values

Good response contracts create a boundary between the internal system and external consumers.

That boundary helps the API evolve safely.

Validation should be predictable

Validation is not only about rejecting bad input.

It is also about helping the consumer understand what went wrong.

A poor validation response might return a generic message like:

Invalid request

That does not help the client fix the problem.

A better API returns structured validation errors that clearly explain which fields failed and why.

For example:

  • customerId is required

  • orderItems must contain at least one item

  • requestedDeliveryDate cannot be in the past

In ASP.NET Core, validation can be handled through model validation, FluentValidation, filters, middleware, or endpoint-specific logic depending on the architecture.

The important part is consistency.

Every endpoint should not invent its own validation response format.

A predictable validation format makes the API easier to integrate with and easier to debug.

Status codes are part of the contract

HTTP status codes are not just technical details.

They are part of the API contract.

A well-designed API uses status codes consistently.

For example:

  • 200 OK for successful reads or updates where a response body is returned

  • 201 Created when a new resource is created

  • 204 No Content when an operation succeeds without returning content

  • 400 Bad Request for malformed or invalid requests

  • 401 Unauthorized when authentication is missing or invalid

  • 403 Forbidden when the user is authenticated but not allowed

  • 404 Not Found when the resource does not exist

  • 409 Conflict when the request conflicts with current system state

  • 500 Internal Server Error for unexpected server-side failures

A common mistake is returning 200 OK for everything and putting success or failure inside the response body.

That makes the API harder to consume correctly.

Clients should be able to rely on HTTP semantics.

The status code should tell the client what happened at a high level, and the response body should provide useful details when needed.

Authentication and authorization are not the same

Authentication answers the question:

“Who are you?”

Authorization answers the question:

“What are you allowed to do?”

An API may correctly authenticate a user but still fail to enforce proper authorization.

For example, a user may be logged in and allowed to view orders, but that does not mean they should be allowed to view every order in the system.

API design should consider authorization at the resource and operation level.

Questions to ask include:

  • Can this user access this resource?

  • Can this user perform this action?

  • Is access based on role, ownership, tenant, permission, or policy?

  • Should authorization happen at the endpoint, service, or domain level?

  • Are there multi-tenant boundaries?

  • Are sensitive fields protected?

In ASP.NET Core, policies, roles, claims, and custom authorization handlers can help enforce these rules.

But the design decision comes first.

Security should not be added as an afterthought after the endpoint is already built.

Idempotency matters for some operations

Some API operations may be called more than once because of retries, network failures, client timeouts, or user actions.

If the API is not designed carefully, repeated requests can create duplicate data or inconsistent behavior.

For example, if a client sends a payment request and does not receive a response due to a timeout, it may retry the same request.

Should the system create another payment?

Probably not.

That is where idempotency matters.

An idempotent operation can be safely repeated without changing the result beyond the first successful request.

Not every endpoint needs an idempotency strategy.

But for operations involving payments, order creation, external integrations, background processing, or retries, it should be considered early.

Good API design thinks about what happens when the same request is sent twice.

Pagination, filtering, and sorting should be designed consistently

List endpoints often start simple.

GET /orders

Then the data grows.

The client asks for pagination.

Then filtering.

Then sorting.

Then search.

Then date ranges.

Then status filters.

If these patterns are not designed consistently, every list endpoint becomes different.

One endpoint uses page and pageSize.

Another uses skip and take.

Another uses limit and offset.

One returns total count.

Another does not.

One supports sorting by createdDate.

Another supports sortBy.

This inconsistency makes APIs harder to use.

A good API defines list patterns early.

For example:

  • consistent pagination parameters

  • maximum page size limits

  • clear filtering rules

  • supported sort fields

  • default ordering

  • response metadata

  • predictable empty results

List endpoints are often used heavily in real applications, so small design decisions here can have a big impact.

Failure behavior should be designed

APIs do not only communicate success.

They also communicate failure.

A good API design considers what happens when something goes wrong.

For example:

  • the request is invalid

  • the user is not authorized

  • the resource does not exist

  • the resource state does not allow the operation

  • the database is unavailable

  • an external API fails

  • a background process cannot be started

  • a timeout occurs

  • a concurrency conflict happens

Each failure type should be handled intentionally.

Not every failure should become a generic 500 Internal Server Error.

Some failures are expected business situations.

Some are validation issues.

Some are security-related.

Some are infrastructure problems.

Some should be retried.

Some should not.

Clear failure handling makes the API more reliable and easier to support.

Backward compatibility is part of API design

Once clients start using an API, changing it becomes harder.

Removing fields, renaming properties, changing response shapes, or changing status code behavior can break existing consumers.

That is why API design should consider backward compatibility.

A few useful principles:

  • avoid exposing internal models directly

  • be careful when removing or renaming fields

  • prefer additive changes where possible

  • version APIs when necessary

  • document breaking changes clearly

  • avoid changing meaning without changing the contract

Backward compatibility is especially important for APIs consumed by external clients, mobile apps, integrations, or multiple internal teams.

An endpoint may be easy to change in code.

But the API contract may already belong to its consumers.

Observability should not be optional

A good API should be easy to debug in production.

That means observability should be considered during design.

When an issue happens, developers should be able to answer questions like:

  • Which endpoint failed?

  • Which request caused the issue?

  • Which user or tenant was involved?

  • What was the correlation ID?

  • Did validation fail?

  • Did authorization fail?

  • Which downstream dependency failed?

  • How long did the request take?

  • Was the database query slow?

  • Did a retry happen?

  • Was the response successful?

Logs, metrics, traces, and correlation IDs are not just operational extras.

They are part of building a supportable API.

A production issue becomes much harder to resolve when the API returns an error but leaves no useful trail behind.

Documentation improves trust

An API that is hard to understand is hard to trust.

Documentation does not need to be perfect, but it should help another developer integrate without guessing.

Good API documentation should explain:

  • endpoint purpose

  • request format

  • response format

  • required fields

  • status codes

  • validation errors

  • authentication requirements

  • authorization rules

  • pagination/filtering/sorting behavior

  • example requests and responses

  • known constraints

Swagger/OpenAPI is useful, but generated documentation alone may not explain the business meaning of the API.

The best documentation combines technical structure with practical context.

A simple API design checklist

Before exposing an endpoint, it helps to ask a few questions.

  • Is the endpoint name and HTTP method appropriate?

  • Is the request contract separate from internal models?

  • Does the response expose only what the client needs?

  • Are validation errors consistent?

  • Are status codes used correctly?

  • Are authentication and authorization rules clear?

  • Is idempotency needed?

  • Are pagination, filtering, and sorting consistent?

  • Are downstream failures handled properly?

  • Is the API backward compatible?

  • Are logs, traces, and correlation IDs available?

  • Is the endpoint documented well enough for another developer to use?

This checklist does not guarantee perfect API design.

But it helps avoid many common mistakes.

Final thoughts

An API endpoint is not the same as API design.

Creating an endpoint is the easy part.

Designing an API that other developers can trust requires more thought.

Good API design considers contracts, validation, status codes, security, idempotency, failure handling, backward compatibility, observability, and documentation.

A successful API should be predictable, secure, consistent, and easy to consume.

One sign of a well-designed API is simple:

Another developer should be able to integrate with it without needing to ask too many questions.

That is the difference between exposing an endpoint and designing an API.

What is one API design mistake you often see in backend projects?

More from this blog

B

Built for Production

3 posts

Built for Production is a technical blog focused on practical software engineering beyond syntax. I write about .NET, Azure, architecture, APIs, AI-assisted development, debugging, security, performance, and the engineering judgment needed to build reliable, maintainable systems.