The first time I had to figure out how to build a REST API in ASP.NET Core, the HTTP methods were not the difficult part. The real challenge was deciding how models, database access, dependency injection, routing, and responses should fit together.
A good API should do more than return JSON. It should expose predictable URLs, use correct HTTP status codes, validate requests, handle failures cleanly, and remain easy to extend.
I will use a simple product catalog to show the complete flow.
Choose Controllers or Minimal APIs First
Before writing an endpoint, I decide which ASP.NET Core API style suits the project.
ASP.NET Core supports both Minimal APIs and controller-based APIs. Microsoft currently recommends Minimal APIs for many new projects because they need less configuration and provide a concise approach to HTTP endpoints. Controllers remain useful when I want traditional object-oriented organization, attributes, filters, and clearly separated controller classes.
For this example, I use controllers because they make every part of the REST architecture easy to see.
For a small microservice, I might choose Minimal APIs instead.
Step 1: Create an ASP.NET Core Web API Project

The quickest way I create a controller-based project is with the .NET CLI:
dotnet new webapi –use-controllers -o ProductApi
cd ProductApi
The –use-controllers option matters. Current ASP.NET Core tooling can create Minimal API projects by default, so this flag explicitly requests the controller architecture. Microsoft documents the same controller-based project approach in its Web API tutorial.
Two files deserve immediate attention.
Program.cs controls dependency injection, middleware, endpoint mapping, and application startup.
appsettings.json provides a standard location for configuration such as connection strings and environment-specific settings.
Understanding these files early makes how to build a REST API in ASP.NET Core much less confusing.
Step 2: Create the Product Data Model

I normally create a Models directory and add Product.cs.
namespace ProductApi.Models
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Description { get; set; } = string.Empty;
}
}
This model represents the resource the API manages.
A request such as:
GET /api/products/5
should return one representation of that product, normally as JSON.
I keep beginner models simple. In a production application, I usually introduce request and response DTOs instead of exposing database entities directly.
That separation becomes valuable when validation rules and public API contracts start changing.
Step 3: Configure Entity Framework Core

For a compact demonstration, install Microsoft’s EF Core InMemory provider:
dotnet add package Microsoft.EntityFrameworkCore.InMemory
Then create Data/ApiDbContext.cs:
using Microsoft.EntityFrameworkCore;
using ProductApi.Models;
namespace ProductApi.Data
{
public class ApiDbContext : DbContext
{
public ApiDbContext(DbContextOptions<ApiDbContext> options)
: base(options) { }
public DbSet<Product> Products { get; set; }
}
}
ApiDbContext gives Entity Framework Core access to the products collection while ASP.NET Core’s dependency injection system supplies the configured context.
Why I Use InMemory Only for the Demo
This distinction gets missed in many beginner tutorials.
Microsoft explicitly states that its EF Core InMemory provider is not designed for production use. It is neither built for robustness nor production database performance.
I use it here because it removes database setup from the learning exercise.
For a real application, I would normally switch to SQL Server, PostgreSQL, or another supported database provider.
Step 4: Register Services and OpenAPI

Next, I configure Program.cs:
using Microsoft.EntityFrameworkCore;
using ProductApi.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddDbContext<ApiDbContext>(options =>
options.UseInMemoryDatabase(“ProductList”));
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
This is one of the most important stages when learning how to build a REST API in ASP.NET Core.
AddControllers() registers controller support.
AddDbContext() registers the database context through dependency injection.
AddOpenApi() enables OpenAPI document generation. ASP.NET Core supports OpenAPI generation for controller-based and Minimal API applications.
MapControllers() then connects incoming HTTP requests with controller routes.
Step 5: Create RESTful CRUD Endpoints
Create Controllers/ProductsController.cs.
Start with the controller definition:
[ApiController]
[Route(“api/[controller]”)]
public class ProductsController : ControllerBase
{
private readonly ApiDbContext _context;
public ProductsController(ApiDbContext context)
{
_context = context;
}
}
ProductsController produces the route /api/products.
The [ApiController] attribute also enables API-focused behavior. Microsoft documents controller APIs as classes derived from ControllerBase.
GET Endpoints
To return every product:
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
return await _context.Products.ToListAsync();
}
To retrieve one product:
[HttpGet(“{id}”)]
public async Task<ActionResult<Product>> GetProduct(int id)
{
var product = await _context.Products.FindAsync(id);
if (product == null)
return NotFound();
return product;
}
Notice the 404 Not Found response. REST API design is not only about performing operations. Clients also need meaningful HTTP responses.
POST, PUT, and DELETE Endpoints
A create endpoint can return 201 Created:
[HttpPost]
public async Task<ActionResult<Product>> PostProduct(Product product)
{
_context.Products.Add(product);
await _context.SaveChangesAsync();
return CreatedAtAction(
nameof(GetProduct),
new { id = product.Id },
product);
}
I would then map PUT /api/products/{id} to updating an existing record and DELETE /api/products/{id} to removing one.
That gives the API the core CRUD pattern:
GET /api/products
GET /api/products/{id}
POST /api/products
PUT /api/products/{id}
DELETE /api/products/{id}
This predictable resource-based design is the part of how to build a REST API in ASP.NET Core that matters beyond ASP.NET itself.
Step 6: Run and Test the ASP.NET Core API
Run the application:
dotnet run
The terminal displays the local HTTP or HTTPS address.
I test every endpoint independently rather than assuming CRUD works because the application compiled.
For example:
curl https://localhost:7123/api/products
I also test invalid IDs, malformed requests, duplicate operations, and missing fields.
Postman, Visual Studio .http files, curl, or another API client all work well.
Testing failure paths early catches more useful problems than repeatedly testing a successful GET.
Step 7: Prepare the REST API for Production
A CRUD demo is only the foundation.
When I move from a tutorial API toward production, I add input validation, DTOs, centralized error handling, authentication, authorization, logging, database migrations, pagination, rate controls, and automated tests.
Security headers may also matter when the API is part of a browser-facing application. Understanding how to create a content security policy is particularly useful when an ASP.NET Core backend serves or supports web applications that load scripts, styles, images, and other browser resources.
I also avoid leaking stack traces, connection details, or internal implementation information through API errors.
For protected endpoints, there is another current ASP.NET Core behavior worth knowing. Starting with ASP.NET Core 10, known API endpoints using cookie authentication return 401 or 403 responses rather than redirecting clients to login pages.
A Small Design Choice That Prevents Bigger Problems
One lesson I learned while building APIs is to avoid binding every public endpoint directly to the persistence model.
It feels convenient initially:
HTTP request → Product entity → database
But that creates tight coupling.
I prefer:
HTTP request → ProductRequest DTO → Product entity → ProductResponse DTO
That extra boundary lets me change database fields without unexpectedly changing the API contract.
It also prevents clients from submitting properties they should never control.
This is one improvement I would make immediately after learning how to build a REST API in ASP.NET Core, even for a relatively small project.
REST API Questions Developers Often Ask
1. How do I build a CRUD REST API in ASP.NET Core?
Create a Web API project, define models, configure storage, register services, create GET, POST, PUT, and DELETE endpoints, then test each response.
2. Should I use Controllers or Minimal APIs in ASP.NET Core?
Microsoft recommends Minimal APIs for many new projects, while controllers remain useful for structured applications that benefit from controller conventions and organization.
3. Can ASP.NET Core REST APIs use SQL Server?
Yes. Replace the InMemory provider with an EF Core SQL Server provider, configure the connection string, and manage schema changes through migrations.
4. Is Swagger the same as OpenAPI in ASP.NET Core?
Not exactly. OpenAPI is the API description specification, while Swagger commonly refers to tooling used to visualize or interact with OpenAPI-described APIs.
Your API Works. Now Make It Production-Worthy
Learning how to build a REST API in ASP.NET Core becomes much easier once I stop treating the project as one large coding task. I separate it into resources, persistence, services, routes, HTTP responses, and testing.
The product example gives you a working foundation, but I would not stop at CRUD.
My next step would be replacing the temporary InMemory database, adding DTO validation, standardizing error responses, securing endpoints, and writing integration tests. That is where a tutorial API starts becoming an API I would trust in a real application.

Leave a Reply