Skip to content
Learni
View all tutorials
Développement Backend

How to Create a REST API with ASP.NET Core in 2026

Lire en français

Introduction

ASP.NET Core is Microsoft's open-source framework for building modern, scalable, and high-performance web applications. In 2026, it remains the top choice for enterprise REST APIs thanks to its speed, cross-platform compatibility (Windows, Linux, macOS), and native integration with C# 13+.

This beginner tutorial guides you step by step to create a complete REST API for managing products: CRUD endpoints, validation, Swagger for testing, and logging. Why is this crucial? APIs power 90% of modern apps (microservices, mobile, SPA frontends). Without solid foundations, you risk security vulnerabilities or poor performance.

We start from the basics (.NET 10 SDK) to production-ready code. At the end, you'll have a copy-pasteable project deployable to Azure or Docker. Estimated time: 30 min. Analogy: like assembling a Lego engine, each block fits perfectly for a smooth result.

Prerequisites

  • .NET 10 SDK installed (download from dotnet.microsoft.com)
  • Code editor: Visual Studio 2022+ or VS Code with C# extension
  • Basic knowledge of HTTP/REST and C# (variables, classes)
  • Terminal (PowerShell, Bash, or CMD)
  • 5 min to verify: dotnet --version should show 10.x.x

Create and run the base project

terminal
dotnet new webapi -o MonApiProduit --no-https
cd MonApiProduit
dotnet restore
dotnet run

This command creates a minimal Web API project with a default WeatherForecast controller, restores NuGet packages, and starts the server on https://localhost:5xxx or http://localhost:5xxx. The --no-https flag skips self-signed certs for beginners. Test the API at http://localhost:5xxx/swagger.

Understanding the project structure

Your project contains:

  • Program.cs: Single entry point (since .NET 6), configures services and middleware.
  • Controllers/: Folder for REST controllers.
  • appsettings.json: Config (ports, logging).
  • Swagger is pre-included for graphical endpoint testing.

Analogy: Program.cs is the conductor, controllers are the soloists. Delete the WeatherForecast controller to start clean.

Configure Program.cs for Swagger and CORS

Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// CORS for frontend
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowAll", policy =>
    {
        policy.AllowAnyOrigin()
              .AllowAnyMethod()
              .AllowAnyHeader();
    });
});

var app = builder.Build();

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

app.UseHttpsRedirection();
app.UseCors("AllowAll");
app.UseAuthorization();
app.MapControllers();

app.Run();

This code sets up the API with controllers, Swagger (UI auto at /swagger), and CORS to allow calls from any origin (great for dev). The middleware pipeline runs sequentially: dev-only Swagger, then CORS before routing. Pitfall: Forgetting CORS blocks frontend fetch requests.

Create the Product model

Models/Produit.cs
namespace MonApiProduit.Models;

public class Produit
{
    public int Id { get; set; }
    public string? Nom { get; set; }
    public decimal Prix { get; set; }
    public string? Description { get; set; }
    public DateTime DateCreation { get; set; } = DateTime.UtcNow;
}

public class CreateProduitDto
{
    public string Nom { get; set; } = string.Empty;
    public decimal Prix { get; set; }
    public string? Description { get; set; }
}

Defines a Product model for the DB (future EF Core) and a DTO for POST requests (prevents overposting). Auto DateCreation for auditing. DTO protects against injections by not exposing Id. Pitfall: Without DTO, attackers can set Id and bypass auto-increment logic.

Add an in-memory service (simulated repo)

To simulate a DB without EF Core (beginner-friendly), create an in-memory repository. It's a Singleton List with thread-safe CRUD methods. Analogy: Like a shared Excel sheet, but locked for multi-access.

Implement the Product repository interface

Services/IProduitRepository.cs
using MonApiProduit.Models;

namespace MonApiProduit.Services;

public interface IProduitRepository
{
    IEnumerable<Produit> GetAll();
    Produit? GetById(int id);
    void Create(Produit produit);
    void Update(int id, Produit produit);
    void Delete(int id);
}

Interface for dependency injection (DI testable). Separates business logic from controllers. In production, implement with EF Core/SQLite.

In-memory repository implementation

Services/ProduitRepository.cs
using MonApiProduit.Models;

namespace MonApiProduit.Services;

public class ProduitRepository : IProduitRepository
{
    private static readonly List<Produit> _produits = new();
    private static int _nextId = 1;

    public IEnumerable<Produit> GetAll() => _produits;

    public Produit? GetById(int id) => _produits.FirstOrDefault(p => p.Id == id);

    public void Create(Produit produit)
    {
        produit.Id = Interlocked.Increment(ref _nextId);
        _produits.Add(produit);
    }

    public void Update(int id, Produit produit)
    {
        var existing = GetById(id);
        if (existing != null)
        {
            existing.Nom = produit.Nom;
            existing.Prix = produit.Prix;
            existing.Description = produit.Description;
        }
    }

    public void Delete(int id)
    {
        var produit = GetById(id);
        if (produit != null)
            _produits.Remove(produit);
    }
}

Mock repo with thread-safe List (Interlocked for Id). FirstOrDefault handles missing items. Update/Delete are null-safe. Pitfall: Without Interlocked, race conditions occur in multi-threaded environments (IIS/Kestrel).

Register the service in DI

Program.cs (addition)
// In builder.Services, before var app =
using MonApiProduit.Services;

builder.Services.AddSingleton<IProduitRepository, ProduitRepository>();

Add this before builder.Build(). Singleton for shared state (like a DB). Use Scoped in production for EF. DI auto-injects into controllers.

Create the ProductsController

Controllers/ProduitsController.cs
using Microsoft.AspNetCore.Mvc;
using MonApiProduit.Models;
using MonApiProduit.Services;

namespace MonApiProduit.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ProduitsController : ControllerBase
{
    private readonly IProduitRepository _repository;

    public ProduitsController(IProduitRepository repository)
    {
        _repository = repository;
    }

    [HttpGet]
    public ActionResult<IEnumerable<Produit>> GetAll()
    {
        return Ok(_repository.GetAll());
    }

    [HttpGet("{id}")]
    public ActionResult<Produit> GetById(int id)
    {
        var produit = _repository.GetById(id);
        if (produit == null)
            return NotFound();
        return Ok(produit);
    }

    [HttpPost]
    public ActionResult<Produit> Create([FromBody] CreateProduitDto dto)
    {
        var produit = new Produit
        {
            Nom = dto.Nom,
            Prix = dto.Prix,
            Description = dto.Description
        };
        _repository.Create(produit);
        return CreatedAtAction(nameof(GetById), new { id = produit.Id }, produit);
    }

    [HttpPut("{id}")]
    public IActionResult Update(int id, [FromBody] CreateProduitDto dto)
    {
        var produit = _repository.GetById(id);
        if (produit == null)
            return NotFound();
        _repository.Update(id, new Produit
        {
            Id = id,
            Nom = dto.Nom,
            Prix = dto.Prix,
            Description = dto.Description
        });
        return NoContent();
    }

    [HttpDelete("{id}")]
    public IActionResult Delete(int id)
    {
        var produit = _repository.GetById(id);
        if (produit == null)
            return NotFound();
        _repository.Delete(id);
        return NoContent();
    }
}

Full RESTful controller (CRUD) with [ApiController] for auto-validation and binding. Constructor injection for DI. Standard HTTP status codes (201 Created, 204 NoContent). Pitfall: Without CreatedAtAction, HATEOAS breaks for clients.

Test the API with Swagger

Run dotnet run again. Open http://localhost:5xxx/swagger:

  • GET /api/Produits: Empty list.
  • POST: {"nom":"Laptop","prix":999.99,"description":"Gaming"} → Note the Id.
  • GET /api/Produits/1, PUT, DELETE.

Swagger auto-generates docs. Add ? for future query params.

Best practices

  • Always use DTOs: Separates input/output, prevents overposting/security leaks.
  • Validation: Add [Required] to DTO + app.UseModelValidation() (auto with ApiController).
  • Logging: Inject ILogger, log errors/exceptions.
  • Async: Switch everything to Task (await _repo.GetAllAsync()) for scalability.
  • Rate limiting: Add builder.Services.AddRateLimiter() in 2026 for DDoS protection.

Common errors to avoid

  • Port in use: Change "Urls": "http://localhost:5000" in launchSettings.json.
  • CORS blocked: Check middleware order (CORS before UseAuthorization).
  • NullReference: Always null-check before Update/Delete.
  • Forced HTTPS: Use --no-https in dev; Let's Encrypt certs in prod.

Next steps

Upgrade to EF Core for a real DB: dotnet ef migrations add Initial. Add JWT auth with Identity. Deploy to Azure App Service.

Check out our Learni ASP.NET Core trainings for advanced masterclasses (SignalR, Blazor, gRPC).