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:

  1. Use nouns for resource URLs.

  2. Use HTTP methods according to their intended semantics.

  3. Return appropriate HTTP status codes.

  4. Use DTOs to define API contracts.

  5. Validate incoming requests.

  6. Keep business logic out of controllers where possible.

  7. Use Dependency Injection for application services and infrastructure components.

  8. Use Entity Framework Core or another appropriate data-access technology for persistence.

  9. Implement authentication and authorization for protected resources.

  10. Use centralized exception handling and structured logging.

  11. Consider API versioning when maintaining multiple API contracts.

  12. 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.