1. Introduction
A simple blog platform is an excellent full-stack exercise for senior developers. It covers CRUD operations, authentication, image uploads, routing, reusable APIs, and clean UI patterns. This article provides a complete, production‑ready implementation using ASP.NET Core Web API on the backend and Angular on the frontend. The approach uses clean architecture, EF Core, repository patterns, and industry‑grade Angular practices.
2. High‑Level Architecture
Angular 17 frontend
ASP.NET Core Web API backend
EF Core ORM with SQL Server
JWT Authentication
Role-based access (Admin, Author, Reader)
Image uploads via IFormFile
Angular rich text editor
Angular routing for listing, viewing, and writing blog posts
3. Database Schema
Tables
Users
UserId (PK)
Username
Email
PasswordHash
Role
BlogPosts
PostId (PK)
Title
Slug
Content
BannerImageUrl
CreatedBy (FK Users)
CreatedOn
UpdatedOn
Comments
CommentId (PK)
PostId (FK BlogPosts)
CommentText
AuthorName
CreatedOn
4. ASP.NET Core Project Structure
/BlogAPI
/Controllers
/Models
/DTOs
/Services
/Repositories
/Data
/Middleware
Program.cs
5. EF Core Models
BlogPost.cs
public class BlogPost
{
public int PostId { get; set; }
public string Title { get; set; }
public string Slug { get; set; }
public string Content { get; set; }
public string BannerImageUrl { get; set; }
public int CreatedBy { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime? UpdatedOn { get; set; }
}
6. DTOs
public class CreatePostDto
{
public string Title { get; set; }
public string Content { get; set; }
public IFormFile BannerImage { get; set; }
}
7. Repository Pattern
public interface IBlogRepository
{
Task<List<BlogPost>> GetPostsAsync();
Task<BlogPost> GetPostBySlugAsync(string slug);
Task<int> CreatePostAsync(BlogPost post);
}
Implementation
public class BlogRepository : IBlogRepository
{
private readonly AppDbContext _ctx;
public BlogRepository(AppDbContext ctx) => _ctx = ctx;
public async Task<List<BlogPost>> GetPostsAsync() =>
await _ctx.BlogPosts.OrderByDescending(x => x.CreatedOn).ToListAsync();
public async Task<BlogPost> GetPostBySlugAsync(string slug) =>
await _ctx.BlogPosts.FirstOrDefaultAsync(x => x.Slug == slug);
public async Task<int> CreatePostAsync(BlogPost post)
{
_ctx.BlogPosts.Add(post);
await _ctx.SaveChangesAsync();
return post.PostId;
}
}
8. Service Layer
public class BlogService
{
private readonly IBlogRepository _repo;
public BlogService(IBlogRepository repo) => _repo = repo;
public async Task<int> CreatePostAsync(CreatePostDto dto, int userId, string uploadPath)
{
string fileName = null;
if (dto.BannerImage != null)
{
fileName = Guid.NewGuid() + Path.GetExtension(dto.BannerImage.FileName);
var filePath = Path.Combine(uploadPath, fileName);
using var stream = new FileStream(filePath, FileMode.Create);
await dto.BannerImage.CopyToAsync(stream);
}
var post = new BlogPost
{
Title = dto.Title,
Content = dto.Content,
BannerImageUrl = fileName,
Slug = dto.Title.ToLower().Replace(" ", "-"),
CreatedBy = userId,
CreatedOn = DateTime.UtcNow
};
return await _repo.CreatePostAsync(post);
}
}
9. API Controller
[ApiController]
[Route("api/blog")]
public class BlogController : ControllerBase
{
private readonly BlogService _service;
private readonly IWebHostEnvironment _env;
public BlogController(BlogService service, IWebHostEnvironment env)
{
_service = service;
_env = env;
}
[HttpPost]
[Authorize(Roles = "Admin,Author")]
public async Task<IActionResult> CreatePost([FromForm] CreatePostDto dto)
{
int userId = int.Parse(User.FindFirst("id").Value);
string uploadPath = Path.Combine(_env.WebRootPath, "uploads");
Directory.CreateDirectory(uploadPath);
var postId = await _service.CreatePostAsync(dto, userId, uploadPath);
return Ok(new { PostId = postId });
}
[HttpGet]
public async Task<IActionResult> GetPosts() => Ok(await _service.GetPostsAsync());
}
10. Angular Frontend Project Structure
/src/app
/core
/services
/models
/pages
/home
/post
/editor
11. Angular Service
@Injectable({ providedIn: 'root' })
export class BlogService {
private baseUrl = environment.api + '/blog';
constructor(private http: HttpClient) { }
getPosts() {
return this.http.get<any[]>(this.baseUrl);
}
createPost(form: FormData) {
return this.http.post(this.baseUrl, form);
}
}
12. Angular Post Editor Component
export class EditorComponent {
form = this.fb.group({
title: ['', Validators.required],
content: ['', Validators.required],
bannerImage: [null]
});
constructor(private fb: FormBuilder, private blogService: BlogService) { }
onFileSelected(event: any) {
const file = event.target.files[0];
this.form.patchValue({ bannerImage: file });
}
submit() {
const fd = new FormData();
fd.append('title', this.form.value.title);
fd.append('content', this.form.value.content);
if (this.form.value.bannerImage)
fd.append('bannerImage', this.form.value.bannerImage);
this.blogService.createPost(fd).subscribe();
}
}
13. Angular Post List Component
export class HomeComponent implements OnInit {
posts: any[] = [];
constructor(private blogService: BlogService) { }
ngOnInit() {
this.blogService.getPosts().subscribe(res => {
this.posts = res;
});
}
}
14. Angular Routing
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'editor', component: EditorComponent },
{ path: 'post/:slug', component: PostComponent }
];
15. Production Considerations
Use Nginx/Apache to serve uploaded images
Enable HTTPS everywhere
Use Angular lazy-loading
Turn on SQL indexes for slug and CreatedOn
Use global exception handling in API
Consider CDN for images
Add caching for GetPosts
Add rate-limiting for comments
16. Future Enhancements
Tagging system
Categories
Draft and publish workflow
Admin dashboard
Multi-author collaboration
Redis caching
Pagination and infinite scroll