DEV Community

Cover image for Building a REST API with ASP.NET Core: A Step-by-Step Guide
Ryan Mitchell
Ryan Mitchell

Posted on

Building a REST API with ASP.NET Core: A Step-by-Step Guide

REST APIs are an essential part of modern application development. They allow web applications, mobile apps, and other software systems to exchange data through standardized HTTP requests. For example, an e-commerce application might use an API to retrieve products, create orders, update customer information, and process inventory changes.

ASP.NET Core provides a framework for building these APIs using C# and the .NET platform. It supports routing, dependency injection, request validation, authentication, and several other capabilities that help developers create maintainable web services.

In this tutorial, we will build a simple REST API for managing products. You will learn how to create a project, define a model, implement CRUD operations, validate incoming data, test endpoints, and identify important security considerations.

What Is a REST API?

A REST API exposes resources through URLs and uses HTTP methods to perform operations on those resources. A resource might represent a product, customer, order, employee, or any other entity managed by an application.

Common HTTP methods include:

  • GET: Retrieve a resource or collection of resources.
  • POST: Create a new resource.
  • PUT: Replace or update an existing resource.
  • PATCH: Apply a partial update to a resource.
  • DELETE: Remove a resource.

For example, a product API might expose the following endpoints:

  • GET /api/products retrieves all products.
  • GET /api/products/1 retrieves a product by its ID.
  • POST /api/products creates a product.
  • PUT /api/products/1 updates a product.
  • DELETE /api/products/1 deletes a product.

A well-designed API uses consistent resource names, appropriate HTTP status codes, clear error responses, and predictable request and response formats.

Microsoft provides an official overview of creating web APIs with ASP.NET Core, including controller-based APIs and Minimal APIs.

Step 1: Create an ASP.NET Core Web API Project

Before starting, install a supported .NET SDK and an editor such as Visual Studio or Visual Studio Code.

Open a terminal and run:

dotnet new webapi -n ProductApi
cd ProductApi
dotnet run
Enter fullscreen mode Exit fullscreen mode

The template creates a project with the initial configuration required to run a web API. Depending on the SDK and template options, the generated project may use Minimal APIs or include example endpoints.

For this tutorial, we will use controllers because they provide a clear structure for organizing request-handling logic as an application grows.

Create a folder named Models and another named Controllers.

You can review the official .NET and ASP.NET Core support policy to choose a supported version and plan upgrades. Using a supported release helps ensure access to security fixes and ongoing maintenance.

Step 2: Create the Product Model

A model defines the data structure that the application will work with.

Inside the Models folder, create a file named Product.cs:

using System.ComponentModel.DataAnnotations;

namespace ProductApi.Models;

public class Product
{
    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Name { get; set; } = string.Empty;

    [Range(0.01, 1000000)]
    public decimal Price { get; set; }

    public bool IsAvailable { get; set; } = true;
}
Enter fullscreen mode Exit fullscreen mode

This model defines four properties:

  • Id identifies each product.
  • Name stores the product name and cannot be empty.
  • Price stores a positive price within the specified range.
  • IsAvailable indicates whether the product is available.

The validation attributes describe basic rules for incoming data. For example, a product name longer than 100 characters should not be accepted.

For a production application, you would generally use separate request DTOs for creating and updating products. This prevents clients from modifying properties they should not control, such as database-generated identifiers or internal administrative fields.

Step 3: Register Controllers in Program.cs

Open Program.cs and configure the application:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
Enter fullscreen mode Exit fullscreen mode

The main configuration steps are:

  • AddControllers() registers controller support.
  • AddEndpointsApiExplorer() enables endpoint discovery.
  • AddSwaggerGen() configures Swagger document generation when the corresponding package is available.
  • UseHttpsRedirection() redirects HTTP requests to HTTPS where configured.
  • MapControllers() maps controller routes to application endpoints.

If the generated project does not include Swagger support, install the required package before using these Swagger configuration methods.

Swagger UI can provide an interactive interface for exploring and testing endpoints during development. It should be configured carefully in deployed environments, especially if the API exposes sensitive functionality.

Step 4: Implement CRUD Operations

Create a file named ProductsController.cs inside the Controllers folder.

For this introductory example, we will store products in a list held in memory. This makes the API easy to understand without introducing database configuration immediately.

using Microsoft.AspNetCore.Mvc;
using ProductApi.Models;

namespace ProductApi.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private static readonly List<Product> Products = new()
    {
        new Product
        {
            Id = 1,
            Name = "Keyboard",
            Price = 49.99m,
            IsAvailable = true
        }
    };

    private static int _nextId = 2;

    [HttpGet]
    public ActionResult<IEnumerable<Product>> GetAll()
    {
        return Ok(Products);
    }

    [HttpGet("{id:int}")]
    public ActionResult<Product> GetById(int id)
    {
        var product = Products.FirstOrDefault(p => p.Id == id);

        if (product is null)
        {
            return NotFound();
        }

        return Ok(product);
    }

    [HttpPost]
    public ActionResult<Product> Create(Product product)
    {
        product.Id = _nextId++;
        Products.Add(product);

        return CreatedAtAction(
            nameof(GetById),
            new { id = product.Id },
            product);
    }

    [HttpPut("{id:int}")]
    public IActionResult Update(int id, Product updatedProduct)
    {
        var product = Products.FirstOrDefault(p => p.Id == id);

        if (product is null)
        {
            return NotFound();
        }

        product.Name = updatedProduct.Name;
        product.Price = updatedProduct.Price;
        product.IsAvailable = updatedProduct.IsAvailable;

        return NoContent();
    }

    [HttpDelete("{id:int}")]
    public IActionResult Delete(int id)
    {
        var product = Products.FirstOrDefault(p => p.Id == id);

        if (product is null)
        {
            return NotFound();
        }

        Products.Remove(product);

        return NoContent();
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's understand the important parts of this controller.

Routing

The [Route("api/[controller]")] attribute defines the base URL. Because the controller is named ProductsController, the route becomes /api/products.

The HTTP attributes determine which method handles each request. For example, [HttpGet("{id:int}")] matches requests such as /api/products/1.

HTTP status codes

The controller returns different status codes depending on the result:

  • 200 OK indicates that a retrieval succeeded.
  • 201 Created indicates that a new product was created.
  • 204 No Content indicates that an update or deletion succeeded without returning a response body.
  • 404 Not Found indicates that the requested product does not exist.
  • 400 Bad Request indicates that the request is invalid.

The CreatedAtAction() method returns a creation response and identifies the URL where the new resource can be retrieved.

Important limitation of the example

The in-memory list is suitable for learning, not for a production API. Data disappears when the application restarts, and concurrent requests can cause consistency problems. The static ID counter is also not a reliable replacement for database-generated identifiers.

For a real application, use a database such as SQL Server or PostgreSQL, with an appropriate persistence layer and concurrency handling.

Step 5: Validate Incoming Requests

An API should not assume that every client sends valid data.

Because the controller uses [ApiController], ASP.NET Core automatically returns a 400 Bad Request response when model validation fails. Microsoft documents this behavior in its web API controller guidance.

For example, the following request contains an invalid price:

{
  "name": "Keyboard",
  "price": -10,
  "isAvailable": true
}
Enter fullscreen mode Exit fullscreen mode

The price violates the model's validation rule, so the framework rejects the request before the controller action executes.

For more robust applications, consider using request DTOs, domain-level validation, consistent error formats, and tests for boundary conditions.

Validation should also cover business rules. A syntactically valid order, for example, might still be invalid if the requested quantity exceeds available inventory.

Step 6: Test the API

Run the project:

dotnet run
Enter fullscreen mode Exit fullscreen mode

Use the local URL displayed in the terminal. If Swagger is configured, open its interface and test the endpoints. Alternatively, use an API client or a command-line tool such as curl.

Retrieve products:

curl -i https://localhost:7000/api/products
Enter fullscreen mode Exit fullscreen mode

The port and protocol may differ in your environment. Use the actual HTTPS URL printed by the application.

Create a product:

curl -i -X POST \
  https://localhost:7000/api/products \
  -H "Content-Type: application/json" \
  -d '{"name":"Mouse","price":25.50,"isAvailable":true}'
Enter fullscreen mode Exit fullscreen mode

The server should return a creation response if the request is valid.

Next, test a request for a nonexistent ID:

curl -i https://localhost:7000/api/products/999
Enter fullscreen mode Exit fullscreen mode

The expected result is 404 Not Found.

A good testing strategy covers successful requests, invalid input, missing resources, unauthorized requests, and unexpected failures. Automated integration tests are especially useful because they help detect regressions when the API changes.

Step 7: Add a Database and Dependency Injection

The next step is to replace the in-memory collection with persistent storage.

Entity Framework Core is commonly used with ASP.NET Core to work with relational databases. Instead of accessing a static list directly from a controller, introduce a database context or a repository/service abstraction.

Register the required services in Program.cs, configure the database connection, and inject the relevant dependency into the controller.

Dependency injection makes it easier to test components independently and change implementation details without rewriting every controller.

The exact database setup depends on your chosen database provider, deployment environment, and migration strategy. Connection strings and credentials should be stored securely rather than hard-coded into source files.

Step 8: Secure the API Before Deployment

A working API is not necessarily a secure API.

The OWASP API Security Top 10 identifies common API security risks. Its 2023 edition highlights authorization problems among the leading concerns: three of the five highest-ranked risks involve authorization-related weaknesses.

This illustrates why access control needs to be considered during design, not added as an afterthought.

Before deploying an API:

  • Require authentication for protected endpoints.
  • Check authorization for each resource and operation.
  • Validate and constrain incoming data.
  • Apply rate limits and resource-consumption controls where appropriate.
  • Avoid returning sensitive information in error responses.
  • Use HTTPS and protect secrets.
  • Log relevant security events without exposing passwords, tokens, or private data.
  • Keep dependencies and the .NET runtime patched.

Authentication determines who the caller is; authorization determines what that caller can do. A validly authenticated user should not automatically be allowed to access every product, customer, or order in the system.

For example, an API that accepts an order ID must verify that the requesting user is permitted to access that specific order. Merely checking that the ID exists is insufficient.

Step 9: Prepare the API for Real-World Use

Before releasing the API, consider additional requirements that become important as traffic and functionality grow.

Pagination: Avoid returning thousands of records in one response. Support page-based or cursor-based retrieval.

Logging and monitoring: Track response times, error rates, and service health so problems can be identified quickly.

Documentation: Describe endpoints, request schemas, authentication requirements, and error responses.

Versioning: Plan how clients will continue working when API contracts change.

Performance: Measure database query execution, network latency, and resource usage before optimizing.

Automated testing: Include unit tests and integration tests in the development workflow.

Deployment: Configure environment-specific settings, health checks, backups, and a rollback strategy.

These practices help transform a learning project into a maintainable service that other applications can depend on.

Final Thoughts

Building a REST API with ASP.NET Core involves more than defining routes and returning JSON. Developers must also consider data validation, persistence, status codes, security, testing, and maintenance.

The example in this guide provides a starting point for understanding controller-based APIs. As your application grows, replace in-memory storage with a database, introduce dependency injection for business logic, implement appropriate authentication and authorization, and add automated tests.

These principles also help when evaluating custom asp net development services for a project: the important questions concern architecture, code quality, security, documentation, and the team's ability to maintain the application over time.

For readers researching technology providers as a separate task, this overview of the best software development companies in texas offers a starting point for comparing companies. It is separate from the technical steps in this tutorial and is not required to build the API.

The most effective way to learn is to extend the sample: connect a database, add authentication, write integration tests, and document the endpoints. Each improvement introduces practical engineering decisions that are valuable in real-world software development.

Top comments (0)