Architecture Pattern: Client-Side Repository & BaaS Abstraction

In un ecosistema tech che corre velocemente, la flessibilità è tutto. Spesso, però, nello sviluppo di applicazioni moderne cadiamo in una trappola invisibile: l'accoppiamento diretto con i servizi Backend-as-a-Service (BaaS) come ad esempio le api dei CMS Headless o Rest.

Per garantire che un'applicazione possa adattarsi ai cambiamenti nel lungo periodo, è necessario isolare la logica di business dalla persistenza. L'obiettivo è introdurre un Layer di Astrazione (o Anti-Corruption Layer) che disaccoppi completamente l'interfaccia utente dalla tecnologia di gestione dei dati.

In questo articolo esploriamo come farlo attraverso il pattern Client-Side Repository & BaaS Abstraction.

 

Il problema: l'accoppiamento Diretto (Anti-Pattern)

Nello sviluppo rapido, è comune importare l'SDK del fornitore (es. Firebase, Supabase, CMS Headless) direttamente nei componenti della UI.

  • Scenario: un componente UserProfile importa l'SDK e interroga il database: sdk.collection('users').find(...) .

Perché è un rischio?

  • Vendor Lock-in: l'applicazione diventa intrinsecamente legata a quel fornitore. Cambiare backend richiede la riscrittura di centinaia di componenti.
  • Domain Leaking: dettagli tecnici (nomi delle tabelle, sintassi delle query, underscore_case) inquinano la logica di business del frontend.
  • Duplicazione: la logica di filtraggio (es. "utenti attivi") viene ripetuta in più punti, rendendo i bug difficili da scovare.

 

La Soluzione: il Pattern Repository lato Client

L'obiettivo è introdurre un Anti-Corruption Layer, una libreria o un modulo intermedio tra il Frontend e l'SDK esterno. Invece di far parlare la UI direttamente con il database, inseriamo un intermediario: il Client Layer.

Il flusso diventa pulito:

  • Frontend (UI): esprime un'intenzione di business (es. getPremiumUsers()). Non sa da dove arrivino i dati.
  • Client Layer (Adapter): traduce questa richiesta nel linguaggio tecnico del fornitore, recupera i dati, li pulisce e li restituisce in un formato perfetto.
    • Riceve la richiesta.
    • La traduce nel linguaggio specifico del fornitore (es. query SQL, filtri NoSQL, chiamate GraphQL).
    • Esegue la chiamata tramite l'SDK (che è incapsulato qui dentro).
    • Riceve i dati "grezzi" (Raw Data).
    • Mappa e Valida i dati in oggetti di dominio puliti (DTO).
  • External Provider: Fornisce solo il servizio dati.

 

I Tre Pilastri del Pattern della manutenibilità

Per implementare correttamente questo pattern e garantirne l'efficacia nel tempo, ci basiamo su tre concetti architetturali fondamentali:

 

A. Unico punto da manutenere

Il Client Layer diventa l'unico luogo dell'applicazione dove risiede la logica specifica dell'SDK (Single Responsibility).

Mentre l'applicazione (Frontend) evolve seguendo le logiche di business e di UI, la gestione del fornitore di dati rimane confinata in questo layer.

Vantaggio: se l'SDK introduce una "breaking change" o cambia la sintassi di una query, l'intervento di correzione è puntuale e localizzato in un solo file, senza dover rincorrere l'errore in decine di componenti sparsi per il progetto.

 

B. Interfacce Unificate

L'applicazione non deve dipendere dai dati grezzi o dai metodi del fornitore, ma da un contratto stabile e definito da noi. Il Client espone modelli di dati (Domain Models) puliti e metodi con firme costanti, indipendentemente da cosa accade "sotto il cofano".

Vantaggio: questo garantisce che il codice che consuma i dati (i componenti) dipenda da astrazioni stabili e non da implementazioni volatili. Se il backend cambia formato data da timestamp a ISO string, l'interfaccia unificata assorbe il cambiamento, mantenendo il resto dell'app funzionante senza modifiche.

 

C. Scalabilità Orizzontale

Isolando l'SDK dietro un'astrazione, abilitiamo la possibilità di avere diverse implementazioni dello stesso servizio.

Il sistema è progettato per accettare nuovi "adattatori" (es. un DirectusClient, un SupabaseClient o un MockClient per i test) senza alterare il funzionamento del frontend.

Vantaggio: il software è aperto all'estensione (possiamo aggiungere nuovi provider o cambiare tecnologia di backend) ma chiuso alle modifiche (non dobbiamo riscrivere la logica di visualizzazione). Questo permette di scalare o migrare l'infrastruttura dati in modo trasparente e controllato.

 

Esempio Pratico: Dependency Inversion in Azione

Per comprendere la differenza tra un semplice "Service Wrapper" e un vero Client Repository Pattern, dobbiamo introdurre un'interfaccia astratta. In questo modo, il componente non sa nemmeno quale classe stia eseguendo il codice.

 

❌ Approccio Errato (Accoppiamento Forte)

Qui il componente importa direttamente l'SDK o una classe concreta che lo wrappa. Se cambiamo DB, dobbiamo toccare il componente.

1// user-list.component.ts
2import { DirectusSdk } from 'some-baas-provider'; // ⛔ DIPENDENZA DIRETTA DALL'SDK
3
4class UserListComponent {
5  async load() {
6    // Il componente è "sporco": conosce i dettagli del DB (collezioni, filtri proprietari)
7    const res = await DirectusSdk.items('sys_users')
8      .readByQuery({ filter: { status: { _eq: 'active' } } });
9      
10    this.users = res.data; 
11  }
12}

 

✅ Approccio Corretto (Repository Pattern & Interfacce)

In questo approccio, separiamo il Cosa (Interfaccia) dal Come (Implementazione Vendor).

Passo 1: definiamo il Contratto (Agnostico)

Questa interfaccia definisce le regole del gioco. Non c'è traccia di Directus, Firebase o SQL. Solo business.

1// libs/domain/user.contract.ts
2import { User } from './user.model';
3
4export interface UserClient {
5  // Il componente chiamerà solo questo metodo.
6  // Non sa se dietro c'è una chiamata API, un DB locale o un file JSON.
7  getActiveUsers(): Promise<User[]>;
8}

 

Passo 2: creiamo l'Implementazione Concreta (Vendor Specific)

Qui, e solo qui, risiede la logica "sporca" del fornitore specifico. Questa classe rispetta il contratto definito sopra.

1// libs/infrastructure/directus-user.client.ts
2import { UserClient } from '@libs/domain';
3import { DirectusSdk } from 'directus-sdk'; // L'SDK è isolato qui
4
5export class DirectusUserClient implements UserClient {
6  
7  async getActiveUsers(): Promise<User[]> {
8    // 1. Traduzione: Converto l'intenzione in linguaggio Directus
9    const response = await DirectusSdk.items('sys_users')
10      .readByQuery({ 
11        filter: { status: { _eq: 'active' } } 
12      });
13
14    // 2. Mapping: Converto il JSON grezzo in Modello pulito
15    return response.data.map(item => ({
16      id: item.uuid,
17      fullName: item.first_name + ' ' + item.last_name,
18      isActive: true
19    }));
20  }
21}

 

Passo 3: il Componente (Consumatore Puro)

Il componente dipende dall'interfaccia UserClient. Non sa che esiste una classe DirectusUserClient.

1// user-list.component.ts
2import { UserClient } from '@libs/domain'; // ✅ Dipende solo dall'interfaccia
3
4class UserListComponent {
5  // Inietto l'interfaccia (Dependency Injection)
6  constructor(private userClient: UserClient) {}
7
8  async load() {
9    // ✅ Business Logic Pura
10    this.users = await this.userClient.getActiveUsers();
11  }
12}

 

Passo 4: il Wiring (la Magia)

Nel modulo principale (es. app.module.ts in Angular o nel container DI), decidiamo quale implementazione usare.

1// app.config.ts
2providers: [
3  { 
4    provide: UserClient, // Quando qualcuno chiede UserClient...
5    useClass: DirectusUserClient // ...dagli l'implementazione Directus!
6  }
7]

 

Il vero vantaggio: se domani passiamo a Supabase, creiamo una classe SupabaseUserClient, cambiamo una riga nella configurazione (useClass: SupabaseUserClient) e l'intera applicazione funzionerà senza aver modificato una sola riga di codice nei componenti.

 

Conclusione: vale sempre la pena?

Adottare questo pattern trasforma l'architettura da "Monolitica/Accoppiata" a Modulare, garantendo manutenibilità e testabilità superiori. Tuttavia, questo disaccoppiamento **ha un costo in termini di verbosità. Richiede la scrittura di interfacce, classi e mapper aggiuntivi (boilerplate) che aumentano il volume di codice iniziale.

Quando NON usarlo

Se state realizzando una POC (Proof of Concept), un prototipo "usa e getta" o un'applicazione semplice dove non è prevista evoluzione o scalabilità futura, questo pattern è Over-Engineering. In questi casi, il tempo speso a scrivere astrazioni non verrà ripagato: l'accesso diretto all'SDK è la scelta più pragmatica ed efficiente.

Scegliete il Repository Pattern solo quando state costruendo software destinato a durare, dove la protezione dall'obsolescenza tecnologica vale l'investimento iniziale.

Hai un progetto in mente?

Contattaci

Autore

Gabriele Bilello

Considera la programmazione più una passione che un lavoro. È la stessa curiosità che spinge molti sviluppatori a esplorare nuovi linguaggi, framework e metodologie ogni giorno.

Per lui programmare è come un videogioco: davanti a ogni bug o requisito c’è un enigma da risolvere, una sfida da affrontare nel modo più elegante ed efficiente possibile. Ama sperimentare e provare cose nuove, soprattutto quando migliorano la developer experience e rendono il lavoro più fluido e “comodo”.

Sviluppa principalmente applicazioni web con Angular, Node.js, NestJS e Astro, ma nel tempo ha lavorato anche con PHP, MySQL, e JavaScript, senza disdegnare incursioni in altri linguaggi e tecnologie come Vue o Flutter.

Nel tempo libero gli piacciono i videogiochi e i gatti — due ottime scuole di pazienza, strategia e problem solving, qualità che tornano sorprendentemente utili anche nel codice.

Devmy su linkedin

Ultimi Articoli