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:
- Progressive Framework: You can adopt Vue.js incrementally - use it for just one part of your page or build entire applications
- Reactive Data Binding: Automatic UI updates when data changes
- Component-Based Architecture: Build encapsulated components that manage their own state
- Virtual DOM: Efficient rendering for optimal performance
- 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:
- Utility-First: Pre-built classes for common CSS properties
- Responsive Design: Built-in responsive design utilities
- Customizable: Highly configurable design system
- Performance: Remove unused CSS automatically
- 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:
- Reduced Boilerplate: Less code to write compared to traditional controllers
- Performance: High performance with minimal overhead
- Simple Syntax: Easy to understand and write
- Built-in Features: Automatic model binding, validation, and OpenAPI support
- 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:
- Simplicity: Single deployment unit, easier to develop and debug
- Performance: No network latency between modules
- Consistency: Single database transactions across modules
- Development Speed: Faster development with shared code and libraries
- Easy Refactoring: Can be split into microservices later if needed
Key Principles:
- Clear Module Boundaries: Each module has well-defined responsibilities
- Loose Coupling: Modules communicate through well-defined interfaces
- High Cohesion: Related functionality stays together within a module
- 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:
- Identify Module Boundaries: Modules become service boundaries
- Extract Database: Give each service its own database
- Add Communication Layer: Replace direct calls with HTTP/messaging
- 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.