REST APIs with C# — Complete Guide
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
Not returning proper status codes: Use
Ok(),Created(),NoContent(),BadRequest(),NotFound()instead of returning raw objects.Exposing internal entities directly: Create DTOs (Data Transfer Objects) that expose only necessary fields instead of returning EF Core entities.
Ignoring idempotency: PUT and DELETE should be idempotent. PUT replaces a resource; calling it multiple times produces the same result.
Not validating input: Always validate request bodies with DataAnnotations or FluentValidation to prevent invalid data.
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
Design a REST API for a library system with Books, Authors, and Members resources.
Implement pagination, sorting, and filtering for a GET /api/items endpoint.
Add HATEOAS links to API responses to make your API self-discoverable.
Challenge: Build an API with content negotiation that returns JSON, XML, or CSV based on the Accept header.
FAQ
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