Skip to content

REST APIs with C# — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about REST APIs with C#. We cover key concepts, practical examples, and best practices to help you master this topic.

Hook

REST APIs are the backbone of modern web communication. C# and ASP.NET Core provide a powerful, convention-based framework for building RESTful services that are self-documenting, versioned, and production-ready. Understanding REST API design principles is essential for any backend developer.

Learning Path

graph LR
  A[REST APIs] --> B[Resource Routing]
  A --> C[HTTP Methods]
  B --> D[Model Binding]
  B --> E[OpenAPI Swagger]
  D --> F[Versioning]
  style A fill:#4a90d9,color:#fff
  style B fill:#4a90d9,color:#fff
  style C fill:#4a90d9,color:#fff
  style D fill:#4a90d9,color:#fff
  style E fill:#4a90d9,color:#fff
  style F fill:#4a90d9,color:#fff

REST Fundamentals

REST APIs map HTTP methods to CRUD operations on resources.

[ApiController]
[Route("api/[controller]")]
public class BooksController : ControllerBase
{
    private static readonly List<Book> Books = new();
    private static int _nextId = 1;

    // GET /api/books
    [HttpGet]
    public ActionResult<IEnumerable<Book>> GetAll()
    {
        return Ok(Books);
    }

    // GET /api/books/{id}
    [HttpGet("{id}")]
    public ActionResult<Book> GetById(int id)
    {
        var book = Books.Find(b => b.Id == id);
        if (book == null) return NotFound();
        return Ok(book);
    }

    // POST /api/books
    [HttpPost]
    public ActionResult<Book> Create(Book book)
    {
        book.Id = _nextId++;
        Books.Add(book);
        return CreatedAtAction(nameof(GetById), new { id = book.Id }, book);
    }

    // PUT /api/books/{id}
    [HttpPut("{id}")]
    public IActionResult Update(int id, Book updated)
    {
        var index = Books.FindIndex(b => b.Id == id);
        if (index == -1) return NotFound();
        Books[index] = updated;
        return NoContent();
    }

    // PATCH /api/books/{id}
    [HttpPatch("{id}")]
    public IActionResult PartialUpdate(int id, JsonPatchDocument<Book> patch)
    {
        var book = Books.Find(b => b.Id == id);
        if (book == null) return NotFound();
        patch.ApplyTo(book);
        return NoContent();
    }

    // DELETE /api/books/{id}
    [HttpDelete("{id}")]
    public IActionResult Delete(int id)
    {
        var removed = Books.RemoveAll(b => b.Id == id);
        if (removed == 0) return NotFound();
        return NoContent();
    }
}

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public string Author { get; set; } = "";
    public string Isbn { get; set; } = "";
    public decimal Price { get; set; }
}

Model Binding and Validation

ASP.NET Core automatically binds request data to action parameters.

[ApiController]
[Route("api/[controller]")]
public class SearchController : ControllerBase
{
    // Query string binding
    // GET /api/search?q=aspnet&page=1&size=20
    [HttpGet]
    public ActionResult Search(
        [FromQuery] string q,
        [FromQuery] int page = 1,
        [FromQuery] int size = 10)
    {
        return Ok(new { Query = q, Page = page, Size = size });
    }

    // Route data binding
    // GET /api/search/books/42
    [HttpGet("books/{id:int}")]
    public ActionResult GetBook(int id) => Ok(new { BookId = id });

    // Header binding
    [HttpGet("headers")]
    public ActionResult GetHeaders(
        [FromHeader(Name = "X-Request-ID")] string requestId)
    {
        return Ok(new { RequestId = requestId });
    }

    // Body binding with validation
    [HttpPost]
    public ActionResult Create([FromBody] CreateBookRequest request)
    {
        if (!ModelState.IsValid)
            return BadRequest(ModelState);

        return Ok(request);
    }
}

public class CreateBookRequest
{
    [Required]
    [StringLength(200, MinimumLength = 1)]
    public string Title { get; set; } = "";

    [Required]
    public string Author { get; set; } = "";

    [Range(0, 10000)]
    public decimal Price { get; set; }
}

OpenAPI / Swagger

Swagger generates interactive API documentation automatically.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new()
    {
        Title = "Books API",
        Version = "v1",
        Description = "A RESTful API for managing books"
    });

    // Include XML comments
    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    c.IncludeXmlComments(xmlPath);
});

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "Books API V1");
    c.RoutePrefix = "docs";
});

app.MapControllers();
app.Run();

API Versioning

Support multiple API versions gracefully.

// Install: dotnet add package Microsoft.AspNetCore.Mvc.Versioning

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
});

// Controller with version
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public ActionResult Get() => Ok("Products V1");
}

[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("2.0")]
public class ProductsControllerV2 : ControllerBase
{
    [HttpGet]
    public ActionResult Get() => Ok("Products V2 with extra fields");
}

Response Caching and ETags

Improve API performance with caching headers.

[HttpGet("{id}")]
[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Client)]
public ActionResult<Product> GetProduct(int id)
{
    var product = _repository.GetById(id);
    if (product == null) return NotFound();

    // ETag based on product version
    var etag = $"""{product.Version}""";
    HttpContext.Response.Headers.ETag = etag;

    if (HttpContext.Request.Headers.IfNoneMatch == etag)
        return StatusCode(304); // Not Modified

    return Ok(product);
}

Common Mistakes

  1. Not returning proper status codes: Use Ok(), Created(), NoContent(), BadRequest(), NotFound() instead of returning raw objects.

  2. Exposing internal entities directly: Create DTOs (Data Transfer Objects) that expose only necessary fields instead of returning EF Core entities.

  3. Ignoring idempotency: PUT and DELETE should be idempotent. PUT replaces a resource; calling it multiple times produces the same result.

  4. Not validating input: Always validate request bodies with DataAnnotations or FluentValidation to prevent invalid data.

  5. Versioning through URL vs headers: URL versioning is simpler and more discoverable. Header versioning is cleaner but harder to test from a browser.

Practice Questions

  1. Design a REST API for a library system with Books, Authors, and Members resources.

  2. Implement pagination, sorting, and filtering for a GET /api/items endpoint.

  3. Add HATEOAS links to API responses to make your API self-discoverable.

  4. Challenge: Build an API with content negotiation that returns JSON, XML, or CSV based on the Accept header.

FAQ

Should I use PUT or PATCH for updates?

Use PUT for full replacement of a resource. Use PATCH for partial updates. PUT is idempotent, PATCH is not necessarily.

How do I handle file downloads in a REST API?

Return a FileStreamResult with the appropriate MIME type. Use Range processing for large file support.

What is the best way to structure API responses?

Use a consistent envelope: { data, success, errors, meta } with pagination metadata for list endpoints.

How do I secure my REST API?

Use HTTPS, authentication (JWT), authorization policies, rate limiting, and input validation. Never expose sensitive data.

Should I use async actions?

Yes, always use async actions for I/O operations. ASP.NET Core fully supports async controller actions.

Mini Project: Book Store API

Build a complete REST API for managing a book store.

using System;
using System.ComponentModel.DataAnnotations;

[ApiController]
[Route("api/books")]
public class BooksController : ControllerBase
{
    private static readonly List<Book> Books = new();
    private static int _nextId = 1;

    [HttpGet]
    public ActionResult<IEnumerable<Book>> GetAll(
        [FromQuery] string? author = null,
        [FromQuery] int page = 1,
        [FromQuery] int size = 10)
    {
        var query = Books.AsEnumerable();

        if (!string.IsNullOrEmpty(author))
            query = query.Where(b => b.Author.Contains(author, StringComparison.OrdinalIgnoreCase));

        var total = query.Count();
        var items = query.Skip((page - 1) * size).Take(size).ToList();

        Response.Headers["X-Total-Count"] = total.ToString();
        return Ok(items);
    }

    [HttpGet("{id}")]
    public ActionResult<Book> GetById(int id)
    {
        var book = Books.Find(b => b.Id == id);
        if (book == null) return NotFound(new { Message = $"Book {id} not found" });
        return Ok(book);
    }

    [HttpPost]
    public ActionResult<Book> Create([FromBody] Book book)
    {
        book.Id = _nextId++;
        Books.Add(book);
        return CreatedAtAction(nameof(GetById), new { id = book.Id }, book);
    }

    [HttpPut("{id}")]
    public IActionResult Update(int id, [FromBody] Book updated)
    {
        var index = Books.FindIndex(b => b.Id == id);
        if (index == -1) return NotFound();
        updated.Id = id;
        Books[index] = updated;
        return NoContent();
    }

    [HttpDelete("{id}")]
    public IActionResult Delete(int id)
    {
        var removed = Books.RemoveAll(b => b.Id == id);
        if (removed == 0) return NotFound();
        return NoContent();
    }
}

// Test the API
var app = WebApplication.Create(args);
app.MapControllers();
app.Run();

Building RESTful APIs with {{< ilink "C#" }} and ASP.NET Core is straightforward and productive. The framework handles routing, model binding, Serialization, and documentation, letting you focus on business logic while creating robust, standards-compliant .NET Web Services.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro