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