ASP.NET Core RESTful API Tutorial: Build a Beginner-Friendly Blog from Scratch
Start a Blog from Scratch with ASP.NET Core RESTful API - Beginner's Guide
Learn how to build a RESTful API in ASP.NET Core with Entity Framework Core. This beginner-friendly guide covers folder structure, database configuration, and CRUD endpoints for a simple blog application.
1. Project Overview
We’ll build a Blog REST API that allows you to:
Create, read, update, and delete (CRUD) blog posts.
(Optionally) Extend to handle user accounts, comments, or categories.
This overview includes:
A typical layered structure (
Controllers
,Services
/Repositories
,Models
,Data Access
) for better maintainability.Integration with a database (using Entity Framework Core).
2. Prerequisites
.NET 6 or above (You can use .NET 5 or .NET Core 3.1, but we’ll assume .NET 6+ in the tutorial).
Visual Studio 2022 or VS Code (or any preferred IDE).
SQL Server or SQLite for the database (any database provider supported by EF Core will work).
Basic knowledge of C#, .NET, HTTP, and REST principles.
3. Creating the ASP.NET Core Project
Open Visual Studio (or VS Code).
Create a new project:
Project Template: “ASP.NET Core Web API”
Name:
BlogApi
Location: Choose your preferred location
Framework:
.NET 6.0
or higher
3. Click Create.
This will generate a default ASP.NET Core Web API project with a Program.cs
and WeatherForecastController.cs
(you can remove or rename the default files if you like).
4. Recommended Folder Structure
A common approach is to separate concerns into different folders. A possible structure is:
BlogApi
├── Controllers
│ └── PostsController.cs
├── Data
│ └── BlogContext.cs
├── Models
│ └── Post.cs
├── DTOs
│ └── PostDto.cs
├── Repositories
│ └── IPostRepository.cs
│ └── PostRepository.cs
├── Services
│ └── IPostService.cs
│ └── PostService.cs
├── Migrations
├── Program.cs
└── appsettings.json
Explanation:
Controllers: Contains the API endpoints.
Models: Database entity classes (representing tables in the database).
DTOs: Data Transfer Objects (classes used to shape/validate the data between client and server).
Repositories: Data access layer (business logic for retrieving/persisting data).
Services: Optionally hold business logic separate from the data access logic. This is common in multi-layer architectures.
Data: Contains the
DbContext
(EF Core database context) and possibly EF Core seeding/migrations logic.
5. Configuring the Database (EF Core)
We’ll set up a simple Post
entity and configure EF Core.
- Install EF Core NuGet Packages
- In the Package Manager Console (Visual Studio):
Install-Package Microsoft.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Tools
If using SQLite, install Microsoft.EntityFrameworkCore.Sqlite
instead of SqlServer
.
2. Create the DbContext (BlogContext
)
// File: Data/BlogContext.cs
using Microsoft.EntityFrameworkCore;
using BlogApi.Models;
namespace BlogApi.Data
{
public class BlogContext : DbContext
{
public BlogContext(DbContextOptions<BlogContext> options) : base(options) { }
public DbSet<Post> Posts { get; set; } // Table for Posts
}
}
3. Create the Entity (Post
)
// File: Models/Post.cs
namespace BlogApi.Models
{
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
}
4. Configure DB in Program.cs
// Program.cs
using BlogApi.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// 1. Add DB Context builder.Services.AddDbContext<BlogContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString(“DefaultConnection”)));
// 2. Add services builder.Services.AddControllers();
// 3. Build and Configure var app = builder.Build();
// 4. Map Controllers app.MapControllers();
// 5. Run app.Run();
5. **Add Connection String in `appsettings.json`**
```json
{
"ConnectionStrings": {
"DefaultConnection": "Server=YOUR_SQL_SERVER;Database=BlogDb;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
6. Creating the Repository Layer
Repositories typically handle all data-related operations (CRUD) so the controllers and services can remain more business-focused.
- Interface
// File: Repositories/IPostRepository.cs
using BlogApi.Models;
namespace BlogApi.Repositories
{
public interface IPostRepository
{
Task<Post> GetPostByIdAsync(int id);
Task<IEnumerable<Post>> GetAllPostsAsync();
Task<Post> CreatePostAsync(Post post);
Task<Post> UpdatePostAsync(Post post);
Task DeletePostAsync(int id);
}
}
2. Implementation
// File: Repositories/PostRepository.cs
using BlogApi.Data;
using BlogApi.Models;
using Microsoft.EntityFrameworkCore;
namespace BlogApi.Repositories
{
public class PostRepository : IPostRepository
{
private readonly BlogContext _context;
public PostRepository(BlogContext context)
{
_context = context;
}
public async Task<Post> GetPostByIdAsync(int id)
{
return await _context.Posts.FindAsync(id);
}
public async Task<IEnumerable<Post>> GetAllPostsAsync()
{
return await _context.Posts.ToListAsync();
}
public async Task<Post> CreatePostAsync(Post post)
{
_context.Posts.Add(post);
await _context.SaveChangesAsync();
return post;
}
public async Task<Post> UpdatePostAsync(Post post)
{
_context.Posts.Update(post);
await _context.SaveChangesAsync();
return post;
}
public async Task DeletePostAsync(int id)
{
var postToDelete = await _context.Posts.FindAsync(id);
if (postToDelete != null)
{
_context.Posts.Remove(postToDelete);
await _context.SaveChangesAsync();
}
}
}
}
3. Register the Repository in Program.cs
builder.Services.AddScoped<IPostRepository, PostRepository>();
7. (Optional) Creating a Service Layer
If you prefer a Service Layer to handle business logic (separate from repository code), you can introduce another interface and class. This helps if you have complex logic or want to keep controllers thin.
- Interface
// File: Services/IPostService.cs
using BlogApi.Models;
namespace BlogApi.Services
{
public interface IPostService
{
Task<Post> GetPost(int id);
Task<IEnumerable<Post>> GetAllPosts();
Task<Post> CreatePost(Post post);
Task<Post> UpdatePost(Post post);
Task DeletePost(int id);
}
}
2. Implementation
// File: Services/PostService.cs
using BlogApi.Models;
using BlogApi.Repositories;
namespace BlogApi.Services
{
public class PostService : IPostService
{
private readonly IPostRepository _postRepository;
public PostService(IPostRepository postRepository)
{
_postRepository = postRepository;
}
public async Task<Post> GetPost(int id) => await _postRepository.GetPostByIdAsync(id);
public async Task<IEnumerable<Post>> GetAllPosts() => await _postRepository.GetAllPostsAsync();
public async Task<Post> CreatePost(Post post) => await _postRepository.CreatePostAsync(post);
public async Task<Post> UpdatePost(Post post) => await _postRepository.UpdatePostAsync(post);
public async Task DeletePost(int id) => await _postRepository.DeletePostAsync(id);
}
}
3. Register the Service
builder.Services.AddScoped<IPostService, PostService>();
8. Creating the Controller
Controllers define the public endpoints. They receive requests, delegate to the service (or repository), and return responses.
// File: Controllers/PostsController.cs
using BlogApi.Models;
using BlogApi.Services;
using Microsoft.AspNetCore.Mvc;
namespace BlogApi.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class PostsController : ControllerBase
{
private readonly IPostService _postService;
public PostsController(IPostService postService)
{
_postService = postService;
}
// GET: api/posts
[HttpGet]
public async Task<IActionResult> GetAllPosts()
{
var posts = await _postService.GetAllPosts();
return Ok(posts);
}
// GET: api/posts/5
[HttpGet("{id}")]
public async Task<IActionResult> GetPost(int id)
{
var post = await _postService.GetPost(id);
if (post == null) return NotFound();
return Ok(post);
}
// POST: api/posts
[HttpPost]
public async Task<IActionResult> CreatePost(Post post)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
var createdPost = await _postService.CreatePost(post);
// Return 201 Created with the URL to the newly created resource
return CreatedAtAction(nameof(GetPost), new { id = createdPost.Id }, createdPost);
}
// PUT: api/posts/5
[HttpPut("{id}")]
public async Task<IActionResult> UpdatePost(int id, Post post)
{
if (id != post.Id) return BadRequest("ID mismatch");
var existingPost = await _postService.GetPost(id);
if (existingPost == null) return NotFound();
existingPost.Title = post.Title;
existingPost.Content = post.Content;
// any other fields
await _postService.UpdatePost(existingPost);
return NoContent(); // or return Ok(updatedPost)
}
// DELETE: api/posts/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeletePost(int id)
{
var existingPost = await _postService.GetPost(id);
if (existingPost == null) return NotFound();
await _postService.DeletePost(id);
return NoContent();
}
}
}
Notes
We are using
CreatedAtAction
to return201 Created
status when creating a resource, which is standard REST practice.NoContent()
for updates and deletes is also a standard approach.
9. (Optional) Using DTOs
Often, we don’t expose the database entity (Post
) directly to the client. Instead, we use DTOs (Data Transfer Objects) to shape input/output data. For instance, you might not want to expose CreatedAt
or Id
, or you want separate classes for creation vs. update. Here’s an example:
// File: DTOs/PostDto.cs
using System.ComponentModel.DataAnnotations;
namespace BlogApi.DTOs
{
public class PostDto
{
public int Id { get; set; }
[Required]
public string Title { get; set; }
[Required]
public string Content { get; set; }
}
}
Your controller methods can then accept and return PostDto
instead of Post
. You’d map between Post
and PostDto
in the service or controller.
10. Applying EF Core Migrations
- Add Migrations
In the Package Manager Console:
Add-Migration InitialCreate
Update-Database
Add-Migration InitialCreate
will generate a migration in theMigrations
folder describing the database schema based on yourBlogContext
and entity classes.Update-Database
applies this migration to your database.
2. Verify
- Check your database (e.g., using SQL Server Management Studio) and confirm the new
Posts
table.
11. Testing the API
You can use Postman, curl, or Swagger (by default, ASP.NET Core scaffolds Swagger in new API projects) to test your endpoints.
- Start the Application
- Press F5 (Visual Studio) or run
dotnet run
from the command line in the project directory.
2. Swagger UI (default local URL typically https://localhost:5001/swagger
or https://localhost:7037/swagger
):
GET /api/posts
GET /api/posts/{id}
POST /api/posts
{
"title": "My First Post",
"content": "This is the content of my first post."
}
PUT /api/posts/{id}
DELETE /api/posts/{id}
3. Verify the database records are created/updated/deleted.
12. Summary & Next Steps
We’ve built:
A basic ASP.NET Core Web API project (
BlogApi
).A clean folder structure separating controllers, models, repositories, services, and data.
A
BlogContext
with EF Core connected to a SQL database.A
Post
entity representing blog posts.A repository and service layer for retrieving/saving data.
A controller providing RESTful endpoints.
Next Steps to Expand Your Blog API:
Add Authentication/Authorization (e.g., JWT) so only authenticated users can create, edit, or delete posts.
Add Comments or Categories: Create
Comment
orCategory
entities, corresponding tables, repository/service, and controllers.Add Validation at the model or DTO level with data annotations.
Implement Paging, Sorting, and Filtering for retrieving multiple posts.
Use Automapper or manual mapping for mapping between entity models and DTOs.
Final Folder Structure Recap
BlogApi
├── Controllers
│ └── PostsController.cs
├── Data
│ └── BlogContext.cs
├── DTOs
│ └── PostDto.cs
├── Migrations
│ └── (Auto-generated migrations)
├── Models
│ └── Post.cs
├── Repositories
│ └── IPostRepository.cs
│ └── PostRepository.cs
├── Services
│ └── IPostService.cs
│ └── PostService.cs
├── Program.cs
├── appsettings.json
└── *.csproj
By following this structured approach and expanding step-by-step, you’ll have a solid foundation for building out your Blog RESTful API and adding new features as you grow.
📌 Stay Updated
Follow me for more design tips and tools! ✨
🐙 GitHub: Follow me for more web development resources.
🔗 LinkedIn: Connect with me for tips and tricks in coding.
✍️ Medium: Follow me for in-depth articles on web development.
📬 Substack: Dive into my newsletter for exclusive insights and updates: