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 --versionshould show 10.x.x
Create and run the base project
dotnet new webapi -o MonApiProduit --no-https
cd MonApiProduit
dotnet restore
dotnet runThis 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
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
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
Implement the Product repository interface
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
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
// 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
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).