Building REST APIs with ASP.NET Core and C#: A Modern Developer’s Guide

In the era of cloud-native development, RESTful APIs form the backbone of digital ecosystems. From mobile applications and frontend single-page apps to microservices and IoT devices, APIs are how modern software talks to itself. As the demand for resilient, scalable, and well-structured APIs continues to grow, so too does the need for tools and frameworks that enable developers to build them efficiently and maintainably – ASP.NET.

Enter ASP.NET Core—Microsoft’s modern, cross-platform framework for building web applications—and C#, one of the most robust, type-safe languages available to backend developers today.

This article provides a detailed, contemporary look at how to build REST APIs with ASP.NET Core and C#, aimed at both newcomers and experienced developers transitioning from older frameworks. We’ll walk through setup, architecture, routing, controllers, dependency injection, data access, and best practices—using a tone and structure that feels like a thoughtful New York Times explainer for the software world.

Why ASP.NET Core for REST APIs?

Since its release, ASP.NET Core has become the de facto choice for building APIs within the Microsoft ecosystem. Here’s why:

  • Cross-platform: Runs on Windows, Linux, and macOS.
  • Performance: Among the fastest web frameworks available, according to TechEmpower benchmarks.
  • Dependency Injection: Built-in support, enabling testability and modular design.
  • Middleware pipeline: Fine control over request/response processing.
  • Minimal API support: For those who prefer a more concise style.

Understanding REST and Its Principles

Before diving into code, it’s important to understand what REST means:

  • Representational State Transfer (REST) is an architectural style.
  • It uses standard HTTP methods: GET, POST, PUT, DELETE, etc.
  • It emphasizes stateless communication and resource-based URLs.
  • It often returns data in JSON format.
  • Responses should include appropriate status codes.

REST is not a strict protocol—it’s a set of conventions that help structure APIs in a predictable, maintainable way.

Setting Up Your Environment

Prerequisites:

  • .NET SDK (latest)
  • Visual Studio or Visual Studio Code
  • Postman or cURL for testing API endpoints
  • SQL Server or SQLite (optional, for data persistence)

Creating a New API Project

Run the following in your terminal:

bashCopyEditdotnet new webapi -n BookStoreApi
cd BookStoreApi

This command creates a new ASP.NET Core Web API project with default files, including a WeatherForecastController example. You can remove or replace it as needed.

The Project Structure

By default, ASP.NET Core includes:

  • Program.cs – Entry point and app configuration.
  • Startup.cs (for older versions) – Middleware and service configuration.
  • Controllers/ – Where your API endpoints live.
  • appsettings.json – Configuration settings like connection strings and logging.
  • wwwroot/ – Static files (not often used in APIs).

In newer versions of .NET, you may find a simplified startup file without Startup.cs. Everything goes into Program.cs.

Building Your First Controller

Let’s start by creating a simple API to manage books.

Step 1: Create a Model

csharpCopyEditpublic class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Author { get; set; }
    public int YearPublished { get; set; }
}

Step 2: Create a Controller

csharpCopyEditusing Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class BooksController : ControllerBase
{
    private static List<Book> books = new()
    {
        new Book { Id = 1, Title = "1984", Author = "George Orwell", YearPublished = 1949 },
        new Book { Id = 2, Title = "The Great Gatsby", Author = "F. Scott Fitzgerald", YearPublished = 1925 }
    };

    [HttpGet]
    public ActionResult<IEnumerable<Book>> GetAll() => books;

    [HttpGet("{id}")]
    public ActionResult<Book> GetById(int id)
    {
        var book = books.FirstOrDefault(b => b.Id == id);
        return book is null ? NotFound() : Ok(book);
    }

    [HttpPost]
    public ActionResult<Book> Create(Book book)
    {
        book.Id = books.Max(b => b.Id) + 1;
        books.Add(book);
        return CreatedAtAction(nameof(GetById), new { id = book.Id }, book);
    }

    [HttpPut("{id}")]
    public IActionResult Update(int id, Book updatedBook)
    {
        var book = books.FirstOrDefault(b => b.Id == id);
        if (book is null) return NotFound();

        book.Title = updatedBook.Title;
        book.Author = updatedBook.Author;
        book.YearPublished = updatedBook.YearPublished;

        return NoContent();
    }

    [HttpDelete("{id}")]
    public IActionResult Delete(int id)
    {
        var book = books.FirstOrDefault(b => b.Id == id);
        if (book is null) return NotFound();

        books.Remove(book);
        return NoContent();
    }
}

Now run the application:

bashCopyEditdotnet run

Test the API at https://localhost:{port}/api/books using Postman or your browser.

Understanding Routing and HTTP Methods

Each method in the controller maps to an HTTP verb:

  • GET /api/books → Retrieves all books
  • GET /api/books/1 → Retrieves book with ID 1
  • POST /api/books → Creates a new book
  • PUT /api/books/1 → Updates book with ID 1
  • DELETE /api/books/1 → Deletes book with ID 1

The [Route] and [Http*] attributes determine how endpoints are exposed.

Using Dependency Injection and Services

Hardcoding logic in controllers is not scalable. Introduce a service layer.

Step 1: Create an Interface

csharpCopyEditpublic interface IBookService
{
    IEnumerable<Book> GetAll();
    Book Get(int id);
    Book Create(Book book);
    void Update(int id, Book book);
    void Delete(int id);
}

Step 2: Implement the Interface

csharpCopyEditpublic class BookService : IBookService
{
    private readonly List<Book> _books = new()
    {
        new Book { Id = 1, Title = "1984", Author = "George Orwell", YearPublished = 1949 }
    };

    public IEnumerable<Book> GetAll() => _books;

    public Book Get(int id) => _books.FirstOrDefault(b => b.Id == id);

    public Book Create(Book book)
    {
        book.Id = _books.Max(b => b.Id) + 1;
        _books.Add(book);
        return book;
    }

    public void Update(int id, Book book)
    {
        var index = _books.FindIndex(b => b.Id == id);
        if (index == -1) return;
        _books[index] = book;
    }

    public void Delete(int id) => _books.RemoveAll(b => b.Id == id);
}

Step 3: Register the Service

In Program.cs:

csharpCopyEditbuilder.Services.AddSingleton<IBookService, BookService>();

Update the controller to use constructor injection:

csharpCopyEditprivate readonly IBookService _bookService;

public BooksController(IBookService bookService)
{
    _bookService = bookService;
}

This pattern improves testability, separation of concerns, and maintainability.

Adding a Database with Entity Framework Core

Replace in-memory lists with a real database:

1. Install EF Core packages:

bashCopyEditdotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Sqlite

2. Create a DbContext:

csharpCopyEditpublic class BookContext : DbContext
{
    public BookContext(DbContextOptions<BookContext> options) : base(options) { }

    public DbSet<Book> Books { get; set; }
}

3. Configure in Program.cs:

csharpCopyEditbuilder.Services.AddDbContext<BookContext>(options =>
    options.UseSqlite("Data Source=books.db"));

4. Update the service to use the database instead of in-memory lists.

Run migrations:

bashCopyEditdotnet ef migrations add InitialCreate
dotnet ef database update

Response Formatting and Status Codes

ASP.NET Core automatically formats responses as JSON, but you can customize this:

csharpCopyEditreturn Ok(new { success = true, data = book });

Use appropriate status codes:

  • 200 OK for successful GET
  • 201 Created for new resources
  • 204 NoContent for successful updates/deletes
  • 404 NotFound for missing resources
  • 400 BadRequest for validation errors

API Versioning and Documentation

Add Swagger for docs:

bashCopyEditdotnet add package Swashbuckle.AspNetCore

In Program.cs:

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

app.UseSwagger();
app.UseSwaggerUI();

Visit /swagger after running your app to view the interactive docs.

Securing the API

Use:

  • Authentication: JWT tokens for identity
  • Authorization: Roles, policies, [Authorize] attribute
  • HTTPS only: Redirect HTTP to HTTPS
  • Rate limiting: Protect against abuse

Testing and Monitoring

  • Unit test controllers and services with xUnit and Moq
  • Use Postman or integration test libraries for end-to-end scenarios
  • Log exceptions and requests using ILogger<>
  • Use Application Insights or other telemetry tools in production

Final Thoughts

Building REST APIs with ASP.NET Core and C# is not just about scaffolding endpoints—it’s about embracing a thoughtful, modern development philosophy. With built-in support for dependency injection, middleware pipelines, and clean architecture, ASP.NET Core gives you the tools to write robust, scalable, and secure APIs out of the box.

Whether you’re building a small project or the backend of a large-scale enterprise system, mastering ASP.NET Core is an investment in clean, sustainable software development.

Read:

Mastering Asynchronous Programming with async and await in C#

Getting Started with LINQ in C#: A Beginner’s Guide

C# for Absolute Beginners: Writing Your First Console Application


FAQs

1. What’s the difference between ASP.NET Core and ASP.NET MVC/Web API?

ASP.NET Core is a modern, cross-platform framework that unifies the features of ASP.NET MVC and Web API into a single framework. Unlike legacy ASP.NET, ASP.NET Core allows you to build APIs, web apps, and microservices using the same infrastructure, with better performance, built-in dependency injection, and full support for cloud-native development.

2. How do I test my REST API built with ASP.NET Core?

You can test your API using:

  • Postman or cURL for manual endpoint testing.
  • Swagger (OpenAPI) for interactive documentation and testing.
  • Unit tests using xUnit, NUnit, or MSTest to test controllers and services.
  • Integration tests using TestServer or WebApplicationFactory to simulate real HTTP calls.

3. What are the best practices for organizing ASP.NET Core API projects?

Best practices include:

  • Use a layered architecture: separate controllers, services, data access, and models.
  • Apply dependency injection to decouple logic.
  • Use DTOs (Data Transfer Objects) for clean API contracts.
  • Implement logging, error handling, and validation consistently across endpoints.
  • Structure your solution with folders like /Controllers, /Services, /Models, /Data, /DTOs.

4. How can I secure my ASP.NET Core REST API?

To secure your API:

  • Use HTTPS for all communications.
  • Implement JWT (JSON Web Token) authentication for stateless security.
  • Use the [Authorize] attribute to protect routes.
  • Define roles or policies for granular access control.
  • Consider rate limiting and API keys for public APIs.

5. Can I deploy ASP.NET Core APIs on Linux or Docker?

Yes. ASP.NET Core is cross-platform and works seamlessly on Windows, Linux, and macOS. You can:

  • Deploy to Linux VMs or containers.
  • Use Docker to containerize your API.
  • Host on cloud platforms like Azure App Service, AWS Elastic Beanstalk, Google Cloud Run, or Kubernetes.

Leave a Comment