Scopri come i microframework permettono di assemblare architetture personalizzate con moduli chiari, middleware e confini—oltre a compromessi, pattern e insidie.

I microframework sono framework web leggeri che si concentrano sull'essenziale: ricevere una richiesta, instradarla al giusto handler e restituire una risposta. A differenza dei framework full‑stack, di solito non includono tutto ciò di cui potresti aver bisogno (pannelli admin, layer ORM/database, generatori di form, lavori in background, flussi di autenticazione). Forniscono invece un core piccolo e stabile e ti permettono di aggiungere solo ciò che il prodotto richiede davvero.
Un framework full‑stack è come comprare una casa completamente arredata: coerente e comoda, ma più difficile da rimodellare. Un microframework è più vicino a uno spazio vuoto ma strutturalmente solido: decidi tu le stanze, i mobili e i servizi.
Questa libertà è ciò che intendiamo per architettura personalizzata—un design di sistema modellato attorno alle esigenze del team, al tuo dominio e ai vincoli operativi. In termini semplici: scegli i componenti (logging, accesso al DB, validazione, auth, elaborazione in background) e decidi come collegarli, invece di accettare una “via giusta” predefinita.
I team spesso optano per i microframework quando vogliono:
Ci concentreremo su come i microframework supportano il design modulare: comporre blocchi, usare middleware e introdurre dependency injection senza trasformare il progetto in un esperimento da laboratorio.
Non confronteremo framework specifici riga per riga né affermeremo che i microframework siano sempre migliori. L’obiettivo è aiutarti a scegliere una struttura in modo deliberato—e a farla evolvere in sicurezza quando i requisiti cambiano.
I microframework funzionano meglio quando tratti l'applicazione come un kit, non come una casa pre‑costruita. Invece di accettare uno stack opinionato, parti da un piccolo core e aggiungi capacità solo quando ne vale il costo.
Un “core” pratico è di solito solo:
È sufficiente per spedire un endpoint API funzionante o una pagina web. Tutto il resto è opzionale fino a quando non hai una ragione concreta.
Quando ti servono autenticazione, validazione o logging, aggiungili come componenti separati—idealmente dietro interfacce chiare. Questo mantiene l'architettura comprensibile: ogni nuovo pezzo dovrebbe rispondere a “quale problema risolve?” e “dove si collega?”.
Esempi di moduli “aggiungi solo se servono”:
All'inizio scegli soluzioni che non ti intrappolino. Preferisci wrapper sottili e configurazione rispetto a magia profonda del framework. Se puoi sostituire un modulo senza riscrivere la logica di business, stai facendo la cosa giusta.
Una semplice definition of done per le scelte architetturali: il team sa spiegare lo scopo di ogni modulo, può sostituirlo in un giorno o due e testarlo indipendentemente.
I microframework restano piccoli per design, il che significa che puoi scegliere gli “organi” della tua applicazione invece di ereditarne un intero corpo. Questo rende l'architettura personalizzata pratica: puoi partire minimale e aggiungere pezzi solo quando emerge un bisogno reale.
La maggior parte delle app basate su microframework inizia con un router che mappa URL a controller (o handler più semplici). I controller possono essere organizzati per funzionalità (billing, account) o per interfaccia (web vs API), a seconda di come vuoi mantenere il codice.
Il middleware tipicamente avvolge il flusso richiesta/risposta ed è il posto migliore per preoccupazioni trasversali:
Poiché il middleware è componibile, puoi applicarlo globalmente (tutto necessita logging) o solo a route specifiche (gli endpoint admin richiedono auth più rigorosa).
I microframework raramente impongono un layer dati, quindi puoi selezionare quello che si adatta al team e al carico di lavoro:
Un buon pattern è mantenere l'accesso ai dati dietro un repository o un service layer, così cambiare strumenti non si propaga attraverso gli handler.
Non tutti i prodotti hanno bisogno di elaborazione asincrona dal giorno uno. Quando serve, aggiungi un job runner e una coda (invio email, processamento video, webhook). Tratta i job in background come un “entry point” separato nella logica di dominio, condividendo gli stessi servizi del layer HTTP invece di duplicare le regole.
Il middleware è dove i microframework danno il maggior valore: ti permette di gestire bisogni trasversali—cose che ogni richiesta dovrebbe avere—senza appesantire ogni route handler. L'obiettivo è semplice: mantenere gli handler concentrati sulla logica di business e lasciare al middleware l'impianto.
Invece di ripetere gli stessi controlli e header in ogni endpoint, aggiungi il middleware una volta. Un handler pulito può apparire così: analizza l'input, chiama un servizio, restituisce una risposta. Tutto il resto—auth, logging, default di validazione, formattazione della risposta—può avvenire prima o dopo.
L'ordine è comportamento. Una sequenza comune e leggibile è:
Se la compressione avviene troppo presto, può perdere errori; se la gestione errori è troppo tardi, rischi di esporre stack trace o formati incoerenti.
X-Request-Id e includilo nei log.{ error, message, requestId }).Raggruppa il middleware per scopo (observability, sicurezza, parsing, shaping della risposta) e applicalo alla giusta scala: globale per regole veramente universali, e middleware per gruppi di route per aree specifiche (es. /admin). Nomina ogni middleware in modo chiaro e documenta l'ordine previsto con un breve commento vicino alla configurazione così i cambi successivi non rompono silenziosamente il comportamento.
Un microframework ti dà un core sottile “request in, response out”. Tutto il resto—accesso al database, caching, email, API di terze parti—dovrebbe essere intercambiabile. Qui entrano in gioco Inversion of Control (IoC) e Dependency Injection (DI), senza trasformare il codice in un esperimento complesso.
Se una feature ha bisogno di un database, è tentante crearne uno direttamente dentro la feature ("new database client qui"). Il rovescio: ogni posto che “va a fare la spesa” è ora legato a quel client specifico.
IoC inverte questo: la tua feature chiede ciò di cui ha bisogno e il wiring dell'app glielo fornisce. La feature diventa più riutilizzabile e più facile da cambiare.
Dependency Injection significa semplicemente passare le dipendenze invece di crearle internamente. In un setup con microframework questo avviene spesso all'avvio:
Non serve un grande contenitore DI per ottenere i vantaggi. Parti con una regola semplice: costruisci le dipendenze in un posto e passale in basso.
Per rendere i componenti intercambiabili, definisci “ciò che ti serve” come una piccola interfaccia, poi scrivi adapter per strumenti specifici.
Pattern d'esempio:
UserRepository (interfaccia): findById, create, listPostgresUserRepository (adapter): implementa quei metodi usando PostgresInMemoryUserRepository (adapter): implementa gli stessi metodi per i testLa tua logica di business conosce solo UserRepository, non Postgres. Sostituire lo storage diventa una scelta di configurazione, non una riscrittura.
La stessa idea vale per API esterne:
PaymentsGatewayStripePaymentsGateway adapterFakePaymentsGateway per lo sviluppo localeI microframework rendono facile spargere la configurazione nei moduli. Resistilo.
Un pattern manutenibile è:
Questo ti dà l'obiettivo principale: sostituire componenti senza riscrivere l'app. Cambiare DB, rimpiazzare un client API o introdurre una coda diventa una piccola modifica nel layer di wiring—mentre il resto del codice resta stabile.
I microframework non impongono una “via unica” per strutturare il codice. Forniscono routing, gestione richiesta/risposta e alcuni punti di estensione ridotti—così puoi adottare pattern che si adattano alla dimensione del team, alla maturità del prodotto e al ritmo dei cambiamenti.
Questo è il setup familiare: controller per le preoccupazioni HTTP, servizi per le regole di business e repository per parlare con il DB.
Si adatta bene quando il dominio è lineare, il team è piccolo o medio e vuoi posti prevedibili dove mettere il codice. I microframework lo supportano naturalmente: le route mappano ai controller, i controller chiamano i servizi e i repository sono wiring manuale leggero.
L'architettura esagonale è utile quando ti aspetti che il sistema sopravviva alle scelte di oggi—database, message bus, API di terze parti o anche l'interfaccia utente.
I microframework funzionano bene qui perché il layer “adapter” è spesso i tuoi handler HTTP più una sottile traduzione nei comandi di dominio. Le porte sono interfacce nel dominio e gli adapter le implementano (SQL, client REST, code). Il framework resta al bordo, non al centro.
Se vuoi chiarezza da microservizi senza l'overhead operativo, il modular monolith è un'ottima opzione. Mantieni un'unica app deployabile ma dividila in moduli funzionali (Billing, Accounts, Notifications) con API pubbliche esplicite.
I microframework facilitano questo perché non auto‑wireano tutto: ogni modulo può registrare proprie route, dipendenze e accesso ai dati, rendendo i confini visibili e più difficili da oltrepassare accidentalmente.
In tutti e tre i pattern il beneficio è lo stesso: scegli le regole—layout delle cartelle, direzione delle dipendenze e confini dei moduli—mentre il microframework fornisce una superficie stabile e piccola su cui agganciarti.
I microframework rendono facile partire in piccolo e restare flessibili, ma non rispondono alla domanda più grande: quale “forma” dovrebbe prendere il sistema? La scelta giusta dipende meno dalla tecnologia e più da dimensione del team, ritmo di rilascio e quanto è dolorosa la coordinazione.
Un monolite viene distribuito come un'unità unica. È spesso la via più veloce per ottenere un prodotto funzionante: una build, un set di log, un unico posto dove fare debug.
Un modular monolith è ancora un unico deployabile, ma internamente separato in moduli chiari (package, contesti limitati, cartelle per feature). È spesso il miglior “passo successivo” quando il codice cresce—soprattutto con i microframework, dove i moduli restano espliciti.
I microservizi dividono il deployable in più servizi. Questo può ridurre l'accoppiamento tra team, ma moltiplica il lavoro operativo.
Separa quando un confine è già reale nel lavoro:
Evita di separare quando è per comodità (“questa cartella è grande”) o quando i servizi condividerebbero le stesse tabelle del DB. È un segnale che non hai ancora trovato un confine stabile.
Un API gateway può semplificare i client (un punto d'ingresso, auth/rate limiting centralizzato). Lo svantaggio: può diventare un collo di bottiglia e un punto singolo di fallimento se diventa troppo “intelligente”.
Le librerie condivise accelerano lo sviluppo (validazione, logging, SDK), ma creano accoppiamento nascosto. Se più servizi devono aggiornare insieme, hai ricreato un monolite distribuito.
I microservizi aggiungono costi ricorrenti: più pipeline di deploy, versioning, service discovery, monitoring, tracing, gestione incidenti e turni on‑call. Se il team non può gestire quella macchina, un modular monolith costruito con componenti microframework è spesso l'architettura più sicura.
Un microframework si concentra sulle cose essenziali: routing, gestione richiesta/risposta e punti di estensione di base.
Un framework full-stack tipicamente include molte funzionalità “batteries included” (ORM, auth, pannello di amministrazione, form, lavori in background). I microframework sacrificano parte della comodità in favore del controllo: aggiungi solo ciò che ti serve e decidi come collegare i pezzi.
I microframework sono indicati quando vuoi:
Un “core utile minimo” di solito include:
Parti da qui, pubblica un endpoint, e poi aggiungi moduli solo quando giustificati (auth, validazione, osservabilità, code).
Il middleware è ideale per preoccupazioni trasversali che si applicano ampiamente, come:
Mantieni i route handler focalizzati sulla logica di business: parse → chiama il servizio → restituisci risposta.
L'ordine cambia il comportamento. Una sequenza comune e affidabile è:
Documenta l'ordine vicino al codice di setup così cambi futuri non spezzano risposte o assunzioni di sicurezza.
Inversion of Control significa che il codice di business non costruisce le proprie dipendenze (non va a “fare la spesa”). Al contrario, il wiring dell'app fornisce ciò che serve.
Praticamente: costruisci il client DB, il logger e i client HTTP all'avvio, poi passateli a servizi/handler. Questo riduce l'accoppiamento e facilita test e sostituzioni.
No. Puoi ottenere la maggior parte dei benefici della DI con un semplice composition root:
Aggiungi un container solo se il grafo di dipendenze diventa difficile da gestire manualmente—non iniziare con complessità inutili.
Metti storage e API esterne dietro piccole interfacce (porte), poi implementa adapter:
UserRepository con findById, create, listPostgresUserRepository per produzioneUna struttura pratica che mantiene visibili i confini:
app/ composition root (wiring)modules/ moduli funzionali (capabilità di dominio)transport/ routing HTTP + mapping richiesta/rispostaDai priorità a test unitari veloci per le regole di business, poi aggiungi pochi test di integrazione ad alto valore che esercitano la pipeline completa (routing → middleware → handler → persistenza).
Usa DI/fake per isolare servizi esterni e testa il middleware come una pipeline (asserisci header, effetti collaterali e comportamento di blocco). Se più team dipendono da API, aggiungi test di contratto per evitare rotture.
InMemoryUserRepository per i testHandler/servizi dipendono dall'interfaccia, non dall'implementazione concreta. Cambiare database o provider diventa una modifica di wiring/config, non una riscrittura.
shared/tests/Enfatizza API pubbliche dei moduli (es. modules/users/public.ts) ed evita import diretti negli internals di altri moduli.