Skip to content

Part 1: Project Setup & StructureΒΆ

Time: 30 minutes | Prerequisites: Python 3.11+, Poetry

Welcome to the Mario's Pizzeria tutorial series! In this first part, you'll set up your development environment and understand the project structure that supports clean architecture principles.

🎯 What You'll Learn¢

By the end of this tutorial, you'll understand:

  • How to structure a Neuroglia application using clean architecture layers
  • The role of the WebApplicationBuilder in bootstrapping applications
  • How dependency injection works in the framework
  • How to configure multiple sub-applications (API + UI)

πŸ“¦ Project SetupΒΆ

1. Install DependenciesΒΆ

First, let's install the framework and required packages:

# Create a new project directory
mkdir mario-pizzeria
cd mario-pizzeria

# Initialize poetry project
poetry init -n

# Add Neuroglia framework
poetry add neuroglia

# Add additional dependencies
poetry add fastapi uvicorn motor pymongo
poetry add python-multipart jinja2  # For UI support
poetry add starlette  # For session middleware

# Install all dependencies
poetry install

2. Create Directory StructureΒΆ

Neuroglia enforces clean architecture with strict layer separation. Create this structure:

mario-pizzeria/
β”œβ”€β”€ main.py                    # Application entry point
β”œβ”€β”€ api/                       # 🌐 API Layer (REST controllers, DTOs)
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ controllers/
β”‚   β”‚   └── __init__.py
β”‚   └── dtos/
β”‚       └── __init__.py
β”œβ”€β”€ application/               # πŸ’Ό Application Layer (Commands, Queries, Handlers)
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ commands/
β”‚   β”‚   └── __init__.py
β”‚   β”œβ”€β”€ queries/
β”‚   β”‚   └── __init__.py
β”‚   β”œβ”€β”€ events/
β”‚   β”‚   └── __init__.py
β”‚   └── services/
β”‚       └── __init__.py
β”œβ”€β”€ domain/                    # πŸ›οΈ Domain Layer (Entities, Business Rules)
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ entities/
β”‚   β”‚   └── __init__.py
β”‚   └── repositories/
β”‚       └── __init__.py
β”œβ”€β”€ integration/               # πŸ”Œ Integration Layer (Database, External APIs)
β”‚   β”œβ”€β”€ __init__.py
β”‚   └── repositories/
β”‚       └── __init__.py
└── ui/                        # Web UI (templates, static files)
    β”œβ”€β”€ controllers/
    β”‚   └── __init__.py
    └── templates/

Why this structure?

The Dependency Rule states that dependencies only point inward:

API β†’ Application β†’ Domain ← Integration
  • Domain: Pure business logic, no external dependencies
  • Application: Orchestrates domain logic, defines use cases
  • Integration: Implements technical concerns (database, HTTP clients)
  • API: Exposes functionality via REST endpoints

This separation makes code testable, maintainable, and replaceable.

πŸš€ Creating the Application Entry PointΒΆ

Let's create main.py - the heart of your application:

#!/usr/bin/env python3
"""
Mario's Pizzeria - A Clean Architecture Sample Application
"""

import logging
from neuroglia.hosting.web import WebApplicationBuilder, SubAppConfig
from neuroglia.mediation import Mediator
from neuroglia.mapping import Mapper
from neuroglia.serialization.json import JsonSerializer

# Configure logging
logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)

def create_pizzeria_app():
    """
    Create Mario's Pizzeria application using WebApplicationBuilder.

    This demonstrates the framework's opinionated approach to application
    bootstrapping with dependency injection and modular configuration.
    """

    # 1️⃣ Create the application builder
    builder = WebApplicationBuilder()

    # 2️⃣ Configure core framework services
    # Mediator: Handles commands and queries (CQRS pattern)
    Mediator.configure(
        builder,
        [
            "application.commands",   # Command handlers
            "application.queries",    # Query handlers
            "application.events"      # Event handlers
        ]
    )

    # Mapper: Object-to-object transformations (entities ↔ DTOs)
    Mapper.configure(
        builder,
        [
            "application.mapping",
            "api.dtos",
            "domain.entities"
        ]
    )

    # JsonSerializer: Type-aware JSON serialization
    JsonSerializer.configure(
        builder,
        [
            "domain.entities"
        ]
    )

    # 3️⃣ Configure sub-applications
    # API sub-app: REST API with JSON responses
    builder.add_sub_app(
        SubAppConfig(
            path="/api",                    # Mounted at /api prefix
            name="api",
            title="Mario's Pizzeria API",
            description="Pizza ordering REST API",
            version="1.0.0",
            controllers=["api.controllers"],  # Auto-discover controllers
            docs_url="/docs",                # OpenAPI docs at /api/docs
        )
    )

    # UI sub-app: Web interface with templates
    builder.add_sub_app(
        SubAppConfig(
            path="/",                       # Mounted at root
            name="ui",
            title="Mario's Pizzeria UI",
            description="Pizza ordering web interface",
            version="1.0.0",
            controllers=["ui.controllers"],
            static_files={"/static": "static"},  # Serve static assets
            templates_dir="ui/templates",        # Jinja2 templates
            docs_url=None,                  # No API docs for UI
        )
    )

    # 4️⃣ Build the complete application
    app = builder.build_app_with_lifespan(
        title="Mario's Pizzeria",
        description="Complete pizza ordering system",
        version="1.0.0",
        debug=True
    )

    log.info("πŸ• Mario's Pizzeria is ready!")
    return app


def main():
    """Entry point for running the application"""
    import uvicorn

    print("πŸ• Starting Mario's Pizzeria on http://localhost:8080")
    print("πŸ“– API Documentation: http://localhost:8080/api/docs")
    print("🌐 UI: http://localhost:8080/")

    # Run with hot reload for development
    uvicorn.run(
        "main:app",
        host="0.0.0.0",
        port=8080,
        reload=True,
        log_level="info"
    )


# Create app instance for uvicorn
app = create_pizzeria_app()

if __name__ == "__main__":
    main()

πŸ” Understanding the CodeΒΆ

WebApplicationBuilderΒΆ

The WebApplicationBuilder is the central bootstrapping mechanism in Neuroglia:

builder = WebApplicationBuilder()

It provides:

  • Service Container: Dependency injection with scoped, singleton, transient lifetimes
  • Configuration: Automatic scanning and registration
  • Sub-App Support: Multiple FastAPI apps mounted to a main host
  • Lifespan Management: Startup/shutdown hooks for resources

Mediator PatternΒΆ

The Mediator decouples request handling from execution:

Mediator.configure(builder, ["application.commands", "application.queries"])

Why use mediator?

❌ Without Mediator (tight coupling):

# Controller directly calls service
class PizzaController:
    def __init__(self, pizza_service: PizzaService):
        self.service = pizza_service

    def create_pizza(self, data):
        return self.service.create_pizza(data)  # Direct dependency

βœ… With Mediator (loose coupling):

# Controller sends command to mediator
class PizzaController:
    def __init__(self, mediator: Mediator):
        self.mediator = mediator

    async def create_pizza(self, data):
        command = CreatePizzaCommand(name=data.name)
        return await self.mediator.execute_async(command)  # Mediator routes it

The mediator automatically finds and executes the right handler. Controllers stay thin and testable.

Sub-Application ArchitectureΒΆ

Neuroglia supports multiple FastAPI apps mounted to a main host:

builder.add_sub_app(SubAppConfig(path="/api", ...))   # API at /api
builder.add_sub_app(SubAppConfig(path="/", ...))      # UI at /

Benefits:

  • Separation of Concerns: API logic separate from UI logic
  • Different Authentication: JWT for API, sessions for UI
  • Independent Documentation: OpenAPI docs only for API
  • Static File Handling: Serve assets efficiently for UI

This is Mario's Pizzeria's actual architecture.

πŸ§ͺ Test Your SetupΒΆ

Run the application:

poetry run python main.py

You should see:

πŸ• Starting Mario's Pizzeria on http://localhost:8080
πŸ“– API Documentation: http://localhost:8080/api/docs
🌐 UI: http://localhost:8080/
INFO:     Started server process
INFO:     Waiting for application startup.
πŸ• Mario's Pizzeria is ready!
INFO:     Application startup complete.

Visit http://localhost:8080/api/docs - you'll see the OpenAPI documentation (empty for now).

πŸ“ Key TakeawaysΒΆ

  1. Clean Architecture Layers: Domain β†’ Application β†’ API/Integration
  2. WebApplicationBuilder: Central bootstrapping with DI container
  3. Mediator Pattern: Decouples controllers from business logic
  4. Sub-Applications: Multiple FastAPI apps with different concerns
  5. Auto-Discovery: Framework automatically finds and registers controllers/handlers

πŸš€ What's Next?ΒΆ

In Part 2: Domain Model, you'll learn:

  • How to create domain entities with business rules
  • The difference between Entity and AggregateRoot
  • Domain events and why they matter
  • Value objects for type safety

πŸ’‘ Common IssuesΒΆ

ImportError: No module named 'neuroglia'

# Make sure you're in the poetry shell
poetry install
poetry shell

Port 8080 already in use

# Change the port in main()
uvicorn.run("main:app", port=8081, ...)

Module not found when running

# Run from project root directory
cd mario-pizzeria
poetry run python main.py

Next: Part 2: Domain Model β†’