Passa al contenuto principale

Architettura del Codice

Guida completa alla struttura del monorepo Emblema, pattern di codice, convenzioni e organizzazione dei file.

πŸ—οΈ Struttura Monorepo​

Emblema utilizza un approccio monorepo con pnpm workspaces e Turbo per l'orchestrazione delle build.

emblema/
β”œβ”€β”€ apps/ # Applicazioni principali
β”‚ β”œβ”€β”€ www-emblema/ # App web principale (Next.js 14)
β”‚ β”œβ”€β”€ ui/ # Libreria componenti (Shadcn/UI)
β”‚ β”œβ”€β”€ background-task/ # Worker AI (Python/FastAPI/Celery)
β”‚ β”œβ”€β”€ document-render/ # Rendering documenti (Python/FastAPI)
β”‚ β”œβ”€β”€ keycloakify-starter/ # Tema Keycloak personalizzato
β”‚ β”œβ”€β”€ novu-bridge/ # Bridge notifiche (Node.js)
β”‚ β”œβ”€β”€ mcp-demo/ # Demo MCP server (Python)
β”‚ └── doc-emblema/ # Documentazione (Docusaurus)
β”œβ”€β”€ packages/ # Pacchetti condivisi (vuoto attualmente)
β”œβ”€β”€ config/ # Configurazioni servizi
β”œβ”€β”€ docs/ # Documentazione tecnica
β”œβ”€β”€ scripts/ # Script utilitΓ 
β”œβ”€β”€ templates/ # Template per Plop.js
β”œβ”€β”€ shared-volume/ # Volumi Docker condivisi
└── install/ # Script installazione

πŸ“¦ Configurazione Workspace​

pnpm-workspace.yaml​

packages:
- "apps/*"
- "packages/*"

package.json (root)​

Gestisce i comandi principali del monorepo:

{
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"lint": "turbo lint",
"type-check": "turbo type-check",
"docker:build": "docker compose build",
"plop:service": "plop service-generator",
"security:scan": "./scripts/security-scan scan"
}
}

turbo.json​

Configura task parallelizzazione e caching:

{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "build/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}

🎯 Pattern Architetturali​

1. Servizi Specializzati​

Ogni servizio backend Γ¨ dedicato a una funzione specifica:

# apps/background-task/app/main.py
from fastapi import FastAPI
from .routers import tasks
from .celery_app import celery_app

app = FastAPI(title="Background Task Service")
app.include_router(tasks.router, prefix="/api/v1")

# Un servizio = Una responsabilitΓ 
# - background-task: Elaborazione AI (chunking, embeddings)
# - document-render: Rendering documenti
# - mcp-demo: Model Context Protocol demo
# - novu-bridge: Bridge notifiche

2. Modular Monolith (Frontend)​

L'app frontend Γ¨ organizzata in moduli funzionali:

apps/www-emblema/
β”œβ”€β”€ app/ # Next.js 14 App Router
β”‚ β”œβ”€β”€ [lang]/ # Routing i18n
β”‚ β”œβ”€β”€ api/ # API Routes
β”‚ └── globals.css # Stili globali
β”œβ”€β”€ components/ # Componenti per feature
β”‚ β”œβ”€β”€ chat/ # Chat AI
β”‚ β”œβ”€β”€ document/ # Gestione documenti
β”‚ β”œβ”€β”€ chunk/ # Viewer chunks
β”‚ β”œβ”€β”€ form/ # Componenti form
β”‚ └── common/ # Componenti condivisi
β”œβ”€β”€ hooks/ # Custom hooks
β”œβ”€β”€ lib/ # Utilities
β”œβ”€β”€ registry/ # Registry componenti
β”œβ”€β”€ schema/ # Zod schemas
└── types/ # Type definitions

3. Domain-Driven Design​

Organizzazione per domini business:

// Esempio: Domain Document
components/document/
β”œβ”€β”€ form.tsx # Form creazione/edit
β”œβ”€β”€ list.tsx # Lista documenti
β”œβ”€β”€ view.tsx # Visualizzazione singola
β”œβ”€β”€ data-table.tsx # Tabella con sorting/filter
└── preview.tsx # Preview contenuto

hooks/
β”œβ”€β”€ use-document-manifest.ts # Hook specifico domain
└── use-kb-processing-status.ts

schema/
└── document.ts # Validazione Zod

πŸ”§ Convenzioni di Codice​

1. Naming Conventions​

File e Directory​

# Componenti React: PascalCase
components/ChatMessage.tsx
components/DocumentList.tsx

# Hooks: camelCase con prefisso 'use'
hooks/useChat.ts
hooks/useDocumentManifest.ts

# Utilities: kebab-case
lib/file-utils.ts
lib/upload-error-handler.ts

# API Routes: kebab-case
app/api/v1/chat/route.ts
app/api/v1/documents/[id]/route.ts

# Schema: camelCase
schema/document.ts
schema/knowledgeBase.ts

Variabili e Funzioni​

// Costanti: UPPER_SNAKE_CASE
const MAX_FILE_SIZE = 1073741824;
const DEFAULT_CHUNK_SIZE = 512;

// Funzioni: camelCase
function formatFileSize(bytes: number): string {}
function createDocumentSchema(t: TFunction) {}

// Componenti: PascalCase
export const DocumentForm = () => {};
export const ChatMessage = () => {};

// Hook: camelCase con prefisso
export const useDocumentUpload = () => {};
export const useChatScroll = () => {};

2. Import Organization​

// 1. React e librerie esterne
import React from "react";
import { useState, useEffect } from "react";
import { zodResolver } from "@hookform/resolvers/zod";

// 2. Librerie UI
import { Button } from "@/components/ui/button";
import { Form } from "@/components/ui/form";

// 3. Componenti interni
import { DocumentPreview } from "@/components/document/preview";
import { ChunkEditor } from "@/components/chunk/chunk-editor";

// 4. Hooks e utilities
import { useTranslation } from "@/hooks/use-translation";
import { formatFileSize } from "@/lib/file-utils";

// 5. Types e schemas
import { DocumentSchema, createDocumentSchema } from "@/schema/document";
import { FormRef } from "@/types/form-refs";

3. TypeScript Patterns​

Form Pattern con forwardRef​

import { forwardRef, useImperativeHandle } from 'react';
import { FormRef } from '@/types/form-refs';

interface DocumentFormProps extends BaseFormProps<DocumentSchema> {
onCancel?: () => void;
}

export const DocumentForm = forwardRef<FormRef<DocumentSchema>, DocumentFormProps>(
({ defaultValues, onSubmit, variant = 'page', onCancel }, ref) => {
const { t } = useTranslation();
const form = useForm<DocumentSchema>({
resolver: zodResolver(createDocumentSchema(t)),
defaultValues,
});

useImperativeHandle(ref, () => ({
submit: async () => await form.handleSubmit(onSubmit)(),
reset: (values) => form.reset(values || defaultValues),
getValues: () => form.getValues(),
isDirty: () => form.formState.isDirty,
isValid: () => form.formState.isValid,
}));

return (
<Form {...form}>
{/* Form content */}
</Form>
);
}
);

DocumentForm.displayName = 'DocumentForm';

Schema Factory Pattern​

import { z } from "zod";
import { TFunction } from "i18next";
import { createSchemaFactory } from "@/lib/i18n-schema-factory";

export const createDocumentSchema = (t: TFunction) => {
const v = createSchemaFactory("document")(t);

return z.object({
name: z
.string({
required_error: v.string("name").required(),
invalid_type_error: v.string("name").required(),
})
.min(1, v.string("name").required())
.max(255, v.string("name").maxLength(255)),

description: z
.string()
.max(1000, v.string("description").maxLength(1000))
.optional(),
});
};

export type DocumentSchema = z.infer<ReturnType<typeof createDocumentSchema>>;

Custom Hook Pattern​

import { useState, useEffect } from "react";
import { useQuery } from "swr";

export const useDocumentManifest = (documentId?: string) => {
const [isLoading, setIsLoading] = useState(false);

const { data, error, mutate } = useQuery(
documentId ? `/api/v1/documents/${documentId}/manifest` : null,
async (url) => {
const response = await fetch(url);
if (!response.ok) throw new Error("Failed to fetch manifest");
return response.json();
},
);

const refreshManifest = async () => {
setIsLoading(true);
try {
await mutate();
} finally {
setIsLoading(false);
}
};

return {
manifest: data,
error,
isLoading: isLoading || (!data && !error),
refreshManifest,
};
};

🌐 Internazionalizzazione (i18n)​

Flat Key Structure​

// ❌ Struttura nidificata (evitare)
{
"document": {
"form": {
"name": {
"label": "Nome Documento"
}
}
}
}

// βœ… Struttura piatta (utilizzare)
{
"form.document.name.label": "Nome Documento",
"form.document.name.placeholder": "Inserisci nome documento",
"action.save": "Salva",
"ui.label.createdAt": "Creato il"
}

Pattern di Utilizzo​

const { t } = useTranslation();

// Form labels
t("form.document.name.label"); // "Nome Documento"
t("form.document.name.placeholder"); // "Inserisci nome documento"

// Actions
t("action.save"); // "Salva"
t("action.export.pdf"); // "Esporta PDF"

// Messages
t("message.success.saved"); // "Salvato con successo"
t("message.error.notFound"); // "Non trovato"

// Dynamic content
t("message.itemsFound", { count: 5 }); // "5 elementi trovati"

πŸ—„οΈ State Management​

1. Local State (useState)​

Per stato componente locale:

const [isLoading, setIsLoading] = useState<boolean>(false);
const [selectedItems, setSelectedItems] = useState<string[]>([]);

2. Server State (SWR)​

Per dati dal server:

const { data, error, mutate } = useSWR("/api/v1/documents", fetcher);

3. URL State (Next.js)​

Per filtri e paginazione:

const searchParams = useSearchParams();
const page = searchParams.get("page") || "1";
const filter = searchParams.get("filter") || "";

4. Form State (React Hook Form)​

Per gestione form:

const form = useForm<DocumentSchema>({
resolver: zodResolver(createDocumentSchema(t)),
defaultValues: initialData,
});

🎨 Styling Architecture​

Tailwind CSS + CSS Variables​

/* apps/www-emblema/app/globals.css */
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
}

.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
}

Component Variants (CVA)​

import { cva, type VariantProps } from "class-variance-authority";

const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);

πŸ” Code Generation​

Plop.js Templates​

Generazione automatica codice con pattern consistenti:

# Genera nuovo servizio Python
pnpm plop:service
# Input: service-name
# Output: apps/service-name/ con struttura completa FastAPI

# Genera nuova web app
pnpm plop:web
# Input: app-name
# Output: apps/app-name/ con Next.js 14 setup

Template Struttura Servizio Python​

templates/python-service/
β”œβ”€β”€ Dockerfile.hbs # Docker config multi-stage
β”œβ”€β”€ pyproject.toml.hbs # Python deps con uv
β”œβ”€β”€ app/
β”‚ β”œβ”€β”€ main.py # FastAPI app
β”‚ β”œβ”€β”€ dependencies.py # Shared deps
β”‚ β”œβ”€β”€ tasks.py # Celery tasks (se applicabile)
β”‚ └── celery_app.py # Celery config (se applicabile)
└── docker-compose.yaml.hbs # Compose integration

πŸ“Š Monitoring e Logging​

Structured Logging​

import logging
import structlog

logger = structlog.get_logger(__name__)

logger.info(
"document_processed",
document_id=doc_id,
processing_time=elapsed_time,
chunk_count=len(chunks),
user_id=user_id
)

Error Boundaries​

import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error }: { error: Error }) {
return (
<div role="alert">
<h2>Qualcosa Γ¨ andato storto:</h2>
<pre>{error.message}</pre>
</div>
);
}

export function AppWithErrorBoundary({ children }: { children: React.ReactNode }) {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
{children}
</ErrorBoundary>
);
}

πŸ” Security Patterns​

Input Validation​

// Server-side validation
import { z } from "zod";

const createDocumentSchema = z.object({
name: z.string().min(1).max(255),
file: z
.instanceof(File)
.refine((file) => file.size <= MAX_FILE_SIZE, {
message: "File troppo grande",
}),
});

// API Route
export async function POST(request: Request) {
try {
const formData = await request.formData();
const validatedData = createDocumentSchema.parse({
name: formData.get("name"),
file: formData.get("file"),
});

// Process validated data
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ errors: error.errors }, { status: 400 });
}
throw error;
}
}

Authentication Middleware​

// middleware.ts
import { getToken } from "next-auth/jwt";

export async function middleware(request: NextRequest) {
const token = await getToken({ req: request });

if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/auth/signin", request.url));
}

return NextResponse.next();
}

πŸ“ Best Practices​

1. Component Composition​

// βœ… Composable components
<DataTable
data={documents}
columns={documentColumns}
actions={<DocumentActions />}
filters={<DocumentFilters />}
/>

// ❌ Monolithic component
<DocumentTable
showActions={true}
showFilters={true}
actionType="document"
// ... many props
/>

2. Custom Hooks per Logic Reuse​

// Hook riusabile per upload
export const useFileUpload = () => {
const [progress, setProgress] = useState(0);
const [error, setError] = useState<string | null>(null);

const upload = useCallback(async (file: File) => {
// Upload logic con progress tracking
}, []);

return { upload, progress, error };
};

3. Type-Safe API Calls​

// Typed API response
interface DocumentResponse {
id: string;
name: string;
status: "processing" | "completed" | "failed";
}

// Type-safe fetch
const fetchDocument = async (id: string): Promise<DocumentResponse> => {
const response = await fetch(`/api/v1/documents/${id}`);
if (!response.ok) throw new Error("Failed to fetch document");
return response.json();
};

Questa architettura garantisce:

  • βœ… ScalabilitΓ : Pattern modulari e riusabili
  • βœ… ManutenibilitΓ : Codice organizzato e ben strutturato
  • βœ… Type Safety: TypeScript completo
  • βœ… Performance: Ottimizzazioni Next.js e caching
  • βœ… DX: Ottima developer experience con tooling

Questa pagina ti Γ¨ stata utile?