Featured image of post Building Full Stack Web Applications: Vue.js + Tailwind CSS + ASP.NET Core 9 Minimal API with Modular Monolith Architecture

Building Full Stack Web Applications: Vue.js + Tailwind CSS + ASP.NET Core 9 Minimal API with Modular Monolith Architecture

Learn how to create a full stack web application using Vue.js, Tailwind CSS, and ASP.NET Core 9 Minimal API with Modular Monolith architecture. A comprehensive guide for beginners starting their coding journey.

Introduction: What is Full Stack Development?

Full Stack Development refers to the development of both the frontend (client-side) and backend (server-side) portions of a web application. A full stack developer is someone who can work on both the user interface that users interact with and the server logic that powers the application behind the scenes.

Key Components of Full Stack Development:

  • Frontend (Client-side): What users see and interact with

    • User Interface (UI)
    • User Experience (UX)
    • Client-side logic and interactions
  • Backend (Server-side): The engine that powers the application

    • Server logic and APIs
    • Database management
    • Authentication and security
    • Business logic
  • Database: Where data is stored and managed

  • DevOps: Deployment and infrastructure management

In this tutorial, we’ll build a complete full stack application using modern technologies: Vue.js for the frontend, Tailwind CSS for styling, and ASP.NET Core 9 Minimal API for the backend, all organized using Modular Monolith architecture.

What is Vue.js?

Vue.js is a progressive JavaScript framework for building user interfaces and single-page applications (SPAs). Created by Evan You in 2014, Vue.js has become one of the most popular frontend frameworks alongside React and Angular.

Key Features of Vue.js:

  1. Progressive Framework: You can adopt Vue.js incrementally - use it for just one part of your page or build entire applications
  2. Reactive Data Binding: Automatic UI updates when data changes
  3. Component-Based Architecture: Build encapsulated components that manage their own state
  4. Virtual DOM: Efficient rendering for optimal performance
  5. Easy Learning Curve: Simple syntax that’s easy for beginners to understand

Vue.js Advantages:

  • Small bundle size: Fast loading times
  • Excellent documentation: Comprehensive and beginner-friendly
  • Great ecosystem: Rich set of tools and libraries
  • TypeScript support: Full TypeScript integration
  • Composition API: Modern way to organize component logic

Example Vue.js Component:

<template>
  <div class="user-profile">
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
    <button @click="updateProfile">Update Profile</button>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'

const user = reactive({
  name: 'John Doe',
  email: '[email protected]'
})

const updateProfile = () => {
  // Update user profile logic
  console.log('Profile updated!')
}
</script>

Reference: Vue.js Official Documentation

What is Tailwind CSS?

Tailwind CSS is a utility-first CSS framework that provides low-level utility classes to build custom designs directly in your markup. Instead of writing custom CSS, you compose designs by combining small, single-purpose utility classes.

Key Features of Tailwind CSS:

  1. Utility-First: Pre-built classes for common CSS properties
  2. Responsive Design: Built-in responsive design utilities
  3. Customizable: Highly configurable design system
  4. Performance: Remove unused CSS automatically
  5. Developer Experience: Excellent IDE support and debugging tools

Tailwind CSS Advantages:

  • Rapid Development: Build interfaces quickly without writing custom CSS
  • Consistent Design: Built-in design system ensures consistency
  • Mobile-First: Responsive design by default
  • Small Bundle Size: Only includes the CSS you actually use
  • No Context Switching: Style components without leaving your HTML

Example Tailwind CSS Usage:

<!-- Traditional CSS approach -->
<div class="card">
  <h2 class="card-title">Hello World</h2>
  <p class="card-description">This is a description</p>
  <button class="btn btn-primary">Click me</button>
</div>

<!-- Tailwind CSS approach -->
<div class="bg-white p-6 rounded-lg shadow-md">
  <h2 class="text-2xl font-bold text-gray-900 mb-2">Hello World</h2>
  <p class="text-gray-600 mb-4">This is a description</p>
  <button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
    Click me
  </button>
</div>

Common Tailwind CSS Classes:

  • Layout: flex, grid, container, mx-auto
  • Spacing: p-4 (padding), m-2 (margin), space-y-4
  • Typography: text-xl, font-bold, text-center
  • Colors: bg-blue-500, text-red-600, border-gray-300
  • Responsive: sm:text-lg, md:grid-cols-2, lg:px-8

Reference: Tailwind CSS Official Documentation

What is ASP.NET Core 9 Minimal API?

ASP.NET Core Minimal API is a streamlined approach to building HTTP APIs with ASP.NET Core. Introduced in .NET 6 and enhanced in subsequent versions including .NET 9, Minimal APIs allow you to create APIs with minimal ceremony and boilerplate code.

Key Features of Minimal API:

  1. Reduced Boilerplate: Less code to write compared to traditional controllers
  2. Performance: High performance with minimal overhead
  3. Simple Syntax: Easy to understand and write
  4. Built-in Features: Automatic model binding, validation, and OpenAPI support
  5. Hosting: Integrated with ASP.NET Core hosting model

Minimal API Advantages:

  • Fast Development: Quickly create APIs with minimal setup
  • Microservices Friendly: Perfect for small, focused services
  • Cloud Native: Optimized for containerized environments
  • Modern C# Features: Leverages latest C# language features
  • Integration: Works seamlessly with other .NET libraries

Example Minimal API:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite("DataSource=app.db"));

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// Define API endpoints
app.MapGet("/api/products", async (AppDbContext db) =>
    await db.Products.ToListAsync())
    .WithName("GetProducts")
    .WithOpenApi();

app.MapGet("/api/products/{id}", async (int id, AppDbContext db) =>
    await db.Products.FindAsync(id) is Product product
        ? Results.Ok(product)
        : Results.NotFound())
    .WithName("GetProduct")
    .WithOpenApi();

app.MapPost("/api/products", async (Product product, AppDbContext db) =>
{
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return Results.Created($"/api/products/{product.Id}", product);
})
.WithName("CreateProduct")
.WithOpenApi();

app.Run();

// 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;
}

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
    public DbSet<Product> Products { get; set; }
}

Reference: ASP.NET Core Minimal APIs Documentation

Example Project: E-commerce Product Catalog

Let’s build a simple e-commerce product catalog that demonstrates how all these technologies work together in a modular monolith architecture.

Project Structure:

ECommerceApp/
โ”œโ”€โ”€ frontend/                 # Vue.js + Tailwind CSS
โ”‚   โ”œโ”€โ”€ src/
โ”‚   โ”‚   โ”œโ”€โ”€ components/
โ”‚   โ”‚   โ”œโ”€โ”€ views/
โ”‚   โ”‚   โ”œโ”€โ”€ services/
โ”‚   โ”‚   โ””โ”€โ”€ main.js
โ”‚   โ”œโ”€โ”€ index.html
โ”‚   โ””โ”€โ”€ package.json
โ”œโ”€โ”€ backend/                  # ASP.NET Core 9 Minimal API
โ”‚   โ”œโ”€โ”€ Modules/
โ”‚   โ”‚   โ”œโ”€โ”€ Products/
โ”‚   โ”‚   โ”œโ”€โ”€ Orders/
โ”‚   โ”‚   โ””โ”€โ”€ Users/
โ”‚   โ”œโ”€โ”€ Shared/
โ”‚   โ”‚   โ”œโ”€โ”€ Database/
โ”‚   โ”‚   โ””โ”€โ”€ Infrastructure/
โ”‚   โ”œโ”€โ”€ Program.cs
โ”‚   โ””โ”€โ”€ ECommerceApp.csproj
โ””โ”€โ”€ README.md

Frontend Implementation (Vue.js + Tailwind):

ProductList.vue:

<template>
  <div class="container mx-auto px-4 py-8">
    <h1 class="text-3xl font-bold text-gray-900 mb-8">Product Catalog</h1>
    
    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      <div 
        v-for="product in products" 
        :key="product.id"
        class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"
      >
        <div class="p-6">
          <h3 class="text-xl font-semibold text-gray-900 mb-2">
            {{ product.name }}
          </h3>
          <p class="text-gray-600 mb-4">{{ product.description }}</p>
          <div class="flex justify-between items-center">
            <span class="text-2xl font-bold text-blue-600">
              ${{ product.price }}
            </span>
            <button 
              @click="addToCart(product)"
              class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
            >
              Add to Cart
            </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { productService } from '../services/productService'

const products = ref([])

const fetchProducts = async () => {
  try {
    const response = await productService.getAll()
    products.value = response.data
  } catch (error) {
    console.error('Error fetching products:', error)
  }
}

const addToCart = (product) => {
  // Add to cart logic
  console.log('Added to cart:', product)
}

onMounted(() => {
  fetchProducts()
})
</script>

Product Service:

// services/productService.js
import axios from 'axios'

const API_BASE_URL = 'https://localhost:7070/api'

export const productService = {
  async getAll() {
    return await axios.get(`${API_BASE_URL}/products`)
  },
  
  async getById(id) {
    return await axios.get(`${API_BASE_URL}/products/${id}`)
  },
  
  async create(product) {
    return await axios.post(`${API_BASE_URL}/products`, product)
  }
}

Backend Implementation (ASP.NET Core 9 Minimal API):

Program.cs:

using Microsoft.EntityFrameworkCore;
using ECommerceApp.Modules.Products;
using ECommerceApp.Modules.Orders;
using ECommerceApp.Shared.Database;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));

// Add CORS
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend", policy =>
    {
        policy.WithOrigins("http://localhost:3000")
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Register module services
builder.Services.AddProductModule();
builder.Services.AddOrderModule();

var app = builder.Build();

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseCors("AllowFrontend");

// Map module endpoints
app.MapProductEndpoints();
app.MapOrderEndpoints();

app.Run();

Product Module:

// Modules/Products/ProductModule.cs
using Microsoft.EntityFrameworkCore;
using ECommerceApp.Shared.Database;

namespace ECommerceApp.Modules.Products;

public static class ProductModule
{
    public static IServiceCollection AddProductModule(this IServiceCollection services)
    {
        services.AddScoped<IProductService, ProductService>();
        return services;
    }

    public static WebApplication MapProductEndpoints(this WebApplication app)
    {
        var group = app.MapGroup("/api/products").WithTags("Products");

        group.MapGet("/", GetProducts);
        group.MapGet("/{id}", GetProduct);
        group.MapPost("/", CreateProduct);
        group.MapPut("/{id}", UpdateProduct);
        group.MapDelete("/{id}", DeleteProduct);

        return app;
    }

    private static async Task<IResult> GetProducts(IProductService productService)
    {
        var products = await productService.GetAllAsync();
        return Results.Ok(products);
    }

    private static async Task<IResult> GetProduct(int id, IProductService productService)
    {
        var product = await productService.GetByIdAsync(id);
        return product != null ? Results.Ok(product) : Results.NotFound();
    }

    private static async Task<IResult> CreateProduct(Product product, IProductService productService)
    {
        var createdProduct = await productService.CreateAsync(product);
        return Results.Created($"/api/products/{createdProduct.Id}", createdProduct);
    }

    private static async Task<IResult> UpdateProduct(int id, Product product, IProductService productService)
    {
        if (id != product.Id) return Results.BadRequest();
        
        var updatedProduct = await productService.UpdateAsync(product);
        return updatedProduct != null ? Results.Ok(updatedProduct) : Results.NotFound();
    }

    private static async Task<IResult> DeleteProduct(int id, IProductService productService)
    {
        var success = await productService.DeleteAsync(id);
        return success ? Results.NoContent() : Results.NotFound();
    }
}

Why is Modular Monolith Important?

Modular Monolith combines the benefits of both monolithic and microservices architectures, making it an excellent choice for many applications, especially when starting a new project.

Benefits of Modular Monolith:

  1. Simplicity: Single deployment unit, easier to develop and debug
  2. Performance: No network latency between modules
  3. Consistency: Single database transactions across modules
  4. Development Speed: Faster development with shared code and libraries
  5. Easy Refactoring: Can be split into microservices later if needed

Key Principles:

  1. Clear Module Boundaries: Each module has well-defined responsibilities
  2. Loose Coupling: Modules communicate through well-defined interfaces
  3. High Cohesion: Related functionality stays together within a module
  4. Shared Infrastructure: Common concerns like logging, configuration, database

When to Use Modular Monolith:

  • Starting a new project: Begin with monolith, split later if needed
  • Small to medium teams: Easier coordination and development
  • Uncertain domain boundaries: Learn the domain before splitting
  • Performance requirements: Lower latency than distributed systems
  • Consistency requirements: Need ACID transactions across modules

Example Module Structure:

// Modules/Products/
โ”œโ”€โ”€ ProductModule.cs          # Module configuration and endpoints
โ”œโ”€โ”€ Models/
โ”‚   โ”œโ”€โ”€ Product.cs           # Product entity
โ”‚   โ””โ”€โ”€ ProductDto.cs        # Data transfer objects
โ”œโ”€โ”€ Services/
โ”‚   โ”œโ”€โ”€ IProductService.cs   # Service interface
โ”‚   โ””โ”€โ”€ ProductService.cs    # Service implementation
โ””โ”€โ”€ Data/
    โ””โ”€โ”€ ProductRepository.cs # Data access layer

// Modules/Orders/
โ”œโ”€โ”€ OrderModule.cs
โ”œโ”€โ”€ Models/
โ”œโ”€โ”€ Services/
โ””โ”€โ”€ Data/

// Shared/
โ”œโ”€โ”€ Database/
โ”‚   โ””โ”€โ”€ AppDbContext.cs      # Shared database context
โ”œโ”€โ”€ Infrastructure/
โ”‚   โ”œโ”€โ”€ Logging/
โ”‚   โ””โ”€โ”€ Configuration/
โ””โ”€โ”€ Common/
    โ””โ”€โ”€ BaseEntity.cs        # Shared base classes

Migration Path to Microservices:

If your application grows and you need to split into microservices, the modular structure makes this transition easier:

  1. Identify Module Boundaries: Modules become service boundaries
  2. Extract Database: Give each service its own database
  3. Add Communication Layer: Replace direct calls with HTTP/messaging
  4. Deploy Independently: Each service gets its own deployment pipeline

Conclusion

Building full stack applications with Vue.js, Tailwind CSS, and ASP.NET Core 9 Minimal API using Modular Monolith architecture provides:

  • Modern Development Experience: Latest technologies and best practices
  • Rapid Development: Quick iteration and deployment
  • Scalable Architecture: Can grow from monolith to microservices
  • Great Developer Experience: Excellent tooling and documentation
  • Performance: Fast and efficient applications

This approach is perfect for beginners starting their coding journey because it provides:

  • Clear separation of concerns
  • Modern development practices
  • Scalable architecture
  • Excellent learning resources

Start with this stack, learn the fundamentals, and gradually expand your knowledge as your applications grow in complexity.

References

Built with Hugo
Theme Stack designed by Jimmy