Introduction
RESTful APIs allow applications to communicate using standard HTTP methods such as GET, POST, PUT, and DELETE.
In ASP.NET Core, REST APIs can be built using controllers, routing, model binding, validation, dependency injection, and other built-in framework features.
A well-designed REST API uses meaningful resource-based URLs, appropriate HTTP methods, correct status codes, validation, and structured request and response models.
For example, an API for managing products might expose endpoints such as:
GET /api/products
GET /api/products/1
POST /api/products
PUT /api/products/1
DELETE /api/products/1
Here, products represent the resource being managed.
What Is a RESTful API?
REST stands for Representational State Transfer.
REST is an architectural style for designing networked applications. RESTful APIs commonly use HTTP to expose resources and standard HTTP methods to operate on those resources.
For example:
GET /api/products -> Retrieve products
GET /api/products/10 -> Retrieve product 10
POST /api/products -> Create a product
PUT /api/products/10 -> Update product 10
DELETE /api/products/10 -> Delete product 10
The URL identifies the resource, while the HTTP method describes the intended operation.
Create an ASP.NET Core Web API
Create a new ASP.NET Core Web API project using the .NET CLI:
dotnet new webapi -n ProductApi
cd ProductApi
dotnet run
The project can then be opened in Visual Studio, Visual Studio Code, or another supported development environment.
ASP.NET Core uses controllers to organize API endpoints when the controller-based Web API model is selected.
Create a Products Controller
Create a ProductsController class.
A simple implementation can use an in-memory collection for demonstration purposes:
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private static readonly List<string> Products =
new() { "Laptop", "Mobile" };
[HttpGet]
public IActionResult GetAll()
{
return Ok(Products);
}
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
if (id < 0 || id >= Products.Count)
{
return NotFound();
}
return Ok(Products[id]);
}
[HttpPost]
public IActionResult Create(string product)
{
Products.Add(product);
return CreatedAtAction(
nameof(GetById),
new { id = Products.Count - 1 },
product);
}
}
The controller exposes three endpoints:
GET /api/products
GET /api/products/{id}
POST /api/products
Understand the Controller
[ApiController]
The [ApiController] attribute enables API-specific behavior such as improved parameter binding and automatic model validation.
[ApiController]
[Route("api/[controller]")]
This attribute defines the base route:
[Route("api/[controller]")]
Because the controller is named ProductsController, [controller] resolves to products.
Therefore, the base URL becomes:
/api/products
ControllerBase
API controllers normally inherit from ControllerBase:
public class ProductsController : ControllerBase
ControllerBase provides methods such as Ok(), NotFound(), BadRequest(), and CreatedAtAction() that are commonly used when returning HTTP responses.
Use Nouns in API URLs
RESTful URLs should normally represent resources rather than operations.
Prefer:
/api/products
instead of:
/api/getProducts
Similarly, prefer:
/api/products/10
instead of:
/api/getProductById/10
The HTTP method already communicates the operation.
For example:
GET /api/products
means retrieving products, so there is usually no need to put get in the URL.
Use the Correct HTTP Methods
The standard HTTP methods commonly used in CRUD APIs are:
HTTP Method | Purpose |
|---|---|
GET | Retrieve data |
POST | Create a new resource |
PUT | Replace or update an existing resource |
DELETE | Remove a resource |
For example:
GET /api/products
POST /api/products
PUT /api/products/10
DELETE /api/products/10
Using the methods consistently makes the API easier for clients to understand and consume.
Add PUT and DELETE Operations
The controller can be extended to support updates and deletion.
[HttpPut("{id}")]
public IActionResult Update(int id, string product)
{
if (id < 0 || id >= Products.Count)
{
return NotFound();
}
Products[id] = product;
return Ok(Products[id]);
}
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
if (id < 0 || id >= Products.Count)
{
return NotFound();
}
string product = Products[id];
Products.RemoveAt(id);
return Ok(product);
}
The controller now supports the basic CRUD operations:
Create -> POST
Read -> GET
Update -> PUT
Delete -> DELETE
Return Appropriate HTTP Status Codes
A REST API should communicate the result of an operation through appropriate HTTP status codes.
Common status codes include:
Status Code | Meaning | Typical Usage |
|---|---|---|
200 OK | Request succeeded | Successful GET, PUT, or other response |
201 Created | Resource created | Successful POST |
204 No Content | Successful request with no response body | Successful DELETE or update when no content is returned |
400 Bad Request | Invalid request | Invalid input |
404 Not Found | Resource does not exist | Requested product was not found |
401 Unauthorized | Authentication required or failed | Missing/invalid authentication |
403 Forbidden | Access denied | Authenticated user lacks permission |
500 Internal Server Error | Unexpected server error | Unhandled server-side failure |
For example:
if (id < 0 || id >= Products.Count)
{
return NotFound();
}
returns HTTP 404 when the requested product does not exist.
For a successful creation:
return CreatedAtAction(
nameof(GetById),
new { id = Products.Count - 1 },
product);
returns HTTP 201 Created and provides information about the newly created resource.
Use DTOs for API Models
Real-world APIs should generally avoid exposing internal entity models directly for every request and response.
A Data Transfer Object (DTO) defines the data that an API accepts or returns.
For example:
public class ProductDto
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
The API can use the DTO for creating a product:
[HttpPost]
public IActionResult Create(ProductDto product)
{
if (string.IsNullOrWhiteSpace(product.Name))
{
return BadRequest("Product name is required.");
}
return Created(
"/api/products",
product);
}
DTOs provide a clear API contract and help prevent internal properties from being exposed unintentionally.
Add Request Validation
Validation ensures that an API does not accept invalid input.
Data Annotations can be used with DTOs.
using System.ComponentModel.DataAnnotations;
public class ProductDto
{
[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
[Range(0.01, 100000)]
public decimal Price { get; set; }
}
With [ApiController], invalid model state can automatically result in a validation response.
For example, a request containing an empty product name can be rejected before the controller performs the normal processing.
Use Entity Framework Core for Database Operations
The in-memory list used earlier is useful for demonstrating REST concepts, but production applications normally persist data in a database.
Entity Framework Core can be used to access databases from an ASP.NET Core application.
For example, define a product entity:
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
Create a database context:
using Microsoft.EntityFrameworkCore;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Product> Products => Set<Product>();
}
Register the context through Dependency Injection:
builder.Services.AddDbContext<ApplicationDbContext>(
options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection")));
The controller can then receive the context through constructor injection.
public class ProductsController : ControllerBase
{
private readonly ApplicationDbContext _context;
public ProductsController(ApplicationDbContext context)
{
_context = context;
}
}
This keeps database configuration outside the controller and follows the Dependency Injection approach built into ASP.NET Core.
Example REST API Request Flow
Consider a client creating a product.
The client sends:
POST /api/products
Content-Type: application/json
Request body:
{
"name": "Laptop",
"price": 75000
}
The API processes the request:
Client
|
| POST /api/products
v
ProductsController
|
v
Validation
|
v
Product Service
|
v
Entity Framework Core
|
v
Database
After the product is successfully created, the API can return an HTTP 201 Created response.
Testing the REST API
You can test the endpoints using tools such as Swagger UI, Postman, or another HTTP client.
For example:
Get All Products
GET /api/products
Possible response:
[
{
"id": 1,
"name": "Laptop",
"price": 75000
},
{
"id": 2,
"name": "Mobile",
"price": 30000
}
]
Get a Product by ID
GET /api/products/1
Possible response:
{
"id": 1,
"name": "Laptop",
"price": 75000
}
Create a Product
POST /api/products
Request:
{
"name": "Keyboard",
"price": 1500
}
A successful request should return a 201 Created response.
Update a Product
PUT /api/products/1
Request:
{
"name": "Gaming Laptop",
"price": 85000
}
Delete a Product
DELETE /api/products/1
The API should return an appropriate success response if the resource is deleted successfully.
Production Considerations
A production REST API requires more than controllers and CRUD operations.
Authentication and Authorization
Authentication verifies the identity of the client, while authorization determines what that client is allowed to access.
ASP.NET Core applications can implement authentication using supported mechanisms such as JWT bearer authentication.
For example, protected endpoints can require authorization:
[Authorize]
[HttpGet]
public IActionResult GetAll()
{
// Protected operation
return Ok();
}
Logging
Logging helps developers diagnose application behavior and production issues.
ASP.NET Core provides built-in logging abstractions through ILogger<T>.
public class ProductsController : ControllerBase
{
private readonly ILogger<ProductsController> _logger;
public ProductsController(
ILogger<ProductsController> logger)
{
_logger = logger;
}
}
Exception Handling
Unexpected exceptions should be handled centrally rather than duplicating exception-handling logic throughout every controller.
ASP.NET Core supports middleware-based exception handling, allowing applications to provide consistent error responses.
API Versioning
When an API is consumed by multiple clients, changing an existing contract can cause compatibility problems.
Versioning can help an application introduce changes while maintaining support for existing consumers.
For example:
/api/v1/products
/api/v2/products
The exact versioning strategy should be selected according to the application's requirements and API governance approach.
REST API Design Best Practices
The following practices help create maintainable RESTful APIs:
Use nouns for resource URLs.
Use HTTP methods according to their intended semantics.
Return appropriate HTTP status codes.
Use DTOs to define API contracts.
Validate incoming requests.
Keep business logic out of controllers where possible.
Use Dependency Injection for application services and infrastructure components.
Use Entity Framework Core or another appropriate data-access technology for persistence.
Implement authentication and authorization for protected resources.
Use centralized exception handling and structured logging.
Consider API versioning when maintaining multiple API contracts.
Avoid exposing sensitive internal information in error responses.
Conclusion
Designing RESTful APIs in ASP.NET Core involves more than creating controller actions. A well-designed API uses resource-oriented routing, appropriate HTTP methods, meaningful status codes, validation, DTOs, Dependency Injection, and a structured application architecture.
For simple demonstrations, an in-memory collection can be enough to understand the fundamentals. For production applications, the API can be extended with Entity Framework Core, authentication, authorization, logging, centralized exception handling, and versioning.
When these principles are applied consistently, ASP.NET Core provides a solid foundation for building REST APIs that are easier to consume, test, maintain, and evolve.
Join the conversation! Your thoughts help the community grow.