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

3. Database Schema

Tables

  1. Users

    • UserId (PK)

    • Username

    • Email

    • PasswordHash

    • Role

  2. BlogPosts

    • PostId (PK)

    • Title

    • Slug

    • Content

    • BannerImageUrl

    • CreatedBy (FK Users)

    • CreatedOn

    • UpdatedOn

  3. Comments

    • CommentId (PK)

    • PostId (FK BlogPosts)

    • CommentText

    • AuthorName

    • CreatedOn

4. ASP.NET Core Project Structure

Code
/BlogAPI
  /Controllers
  /Models
  /DTOs
  /Services
  /Repositories
  /Data
  /Middleware
  Program.cs

5. EF Core Models

BlogPost.cs

C#
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

C#
public class CreatePostDto
{
    public string Title { get; set; }
    public string Content { get; set; }
    public IFormFile BannerImage { get; set; }
}

7. Repository Pattern

C#
public interface IBlogRepository
{
    Task<List<BlogPost>> GetPostsAsync();
    Task<BlogPost> GetPostBySlugAsync(string slug);
    Task<int> CreatePostAsync(BlogPost post);
}

Implementation

C#
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

C#
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

C#
[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

Code
/src/app
  /core
  /services
  /models
  /pages
    /home
    /post
    /editor

11. Angular Service

TypeScript
@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

TypeScript
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

TypeScript
export class HomeComponent implements OnInit {
  posts: any[] = [];

  constructor(private blogService: BlogService) { }

  ngOnInit() {
    this.blogService.getPosts().subscribe(res => {
      this.posts = res;
    });
  }
}

14. Angular Routing

TypeScript
const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'editor', component: EditorComponent },
  { path: 'post/:slug', component: PostComponent }
];

15. Production Considerations

16. Future Enhancements