Introduction
Un data catalog est un répertoire centralisé qui inventorie les datasets d'une organisation, enrichis de métadonnées comme la description, le schéma, le propriétaire et les tags. Il facilite la découverte de données, la gouvernance et la collaboration, évitant les silos data. Dans un monde où les volumes de données explosent, un data catalog bien conçu booste la productivité des data teams de 30-50% selon Gartner.
Ce tutoriel vous guide pour créer un data catalog full-stack avec Next.js 15 (App Router), Prisma pour l'ORM, PostgreSQL pour la DB scalable, et une UI simple avec Tailwind CSS. Nous implémenterons :
- Un modèle Dataset avec JSON schema et tags.
- API REST pour CRUD et recherche full-text.
- Interface de listing et ajout avec search.
Résultat : un prototype production-ready, déployable sur Vercel. Temps estimé : 30 min. Idéal pour data engineers intermédiaires cherchant une solution open-source customisable.
Prérequis
- Node.js 20+ installé
- Docker et Docker Compose pour PostgreSQL
- Connaissances de base en TypeScript, Next.js et SQL
- Compte GitHub pour déploiement optionnel
- Outils : VS Code avec extensions Prisma et Tailwind
Initialisation du projet Next.js
npx create-next-app@latest data-catalog --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd data-catalog
npm install prisma @prisma/client
npx prisma init --datasource-provider postgresqlCe script crée un projet Next.js 15 avec TypeScript, Tailwind et App Router. Prisma est installé pour l'ORM, initialisé pour PostgreSQL. Évitez les templates minimaux pour gagner du temps sur la config Tailwind et ESLint.
Configuration de la base PostgreSQL
Lancez un conteneur PostgreSQL dédié via Docker Compose pour isoler l'environnement dev. Copiez le code ci-dessous dans docker-compose.yml à la racine, puis adaptez l'URL de connexion Prisma.
Docker Compose pour PostgreSQL
version: '3.8'
services:
postgres:
image: postgres:16
restart: always
environment:
POSTGRES_DB: datacatalog
POSTGRES_USER: user
POSTGRES_PASSWORD: password
ports:
- '5432:5432'
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:Ce fichier définit un service PostgreSQL persistant avec DB datacatalog. Volume pour éviter la perte de données. Lancez avec docker compose up -d. DATABASE_URL=postgresql://user:password@localhost:5432/datacatalog dans .env.
Schéma Prisma pour le modèle Dataset
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Dataset {
id String @id @default(cuid())
name String @unique
description String?
schema Json?
owner String
tags String[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("datasets")
}Ce schéma définit un modèle Dataset avec champs essentiels : nom unique, description, schéma JSON flexible (pour tables/cols), owner, tags array. @@map pour nom de table custom. Utilisez Json pour schémas dynamiques sans migrations complexes.
Migration et génération du client Prisma
docker compose up -d
cp .env.example .env # Si pas fait
# Éditez .env : DATABASE_URL="postgresql://user:password@localhost:5432/datacatalog"
npx prisma db push
npx prisma generate
npm run devPousse le schéma vers la DB sans migration SQL manuelle (idéal dev). Génère le client TS typé. Lancez npm run dev pour vérifier. Piège : oubliez le generate, sinon les types ne sont pas disponibles dans le code.
Implémentation du client Prisma partagé
Créez un utilitaire singleton pour le client Prisma, évitant les reconnexions inutiles en serverless (Vercel). Placez-le dans src/lib/prisma.ts.
Client Prisma singleton
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prismaSingleton global pour réutiliser l'instance Prisma en hot-reload dev. Essentiel en Next.js serverless pour éviter 100+ connexions DB. En prod, une instance par request cold start.
API Route pour CRUD Datasets
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { z } from 'zod'
const createSchema = z.object({
name: z.string().min(1),
description: z.string().optional(),
schema: z.object({}).passthrough().optional(),
owner: z.string().min(1),
tags: z.array(z.string()).optional().default([]),
})
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const q = searchParams.get('q') || ''
const datasets = await prisma.dataset.findMany({
where: {
OR: [
{ name: { contains: q, mode: 'insensitive' } },
{ description: { contains: q, mode: 'insensitive' } },
{ tags: { has: q } },
],
},
orderBy: { createdAt: 'desc' },
})
return NextResponse.json(datasets)
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const validated = createSchema.parse(body)
const dataset = await prisma.dataset.create({ data: validated })
return NextResponse.json(dataset, { status: 201])
} catch (error) {
return NextResponse.json({ error: 'Validation failed' }, { status: 400 })
}
}Route dynamique GET/POST avec Zod validation. Recherche full-text sur name/desc/tags via Prisma OR. POST crée avec parse safe. Ajoutez npm i zod avant. Piège : sans mode 'insensitive', la search est case-sensitive.
Interface utilisateur pour listing et ajout
Tailwind est préconfiguré. Créez la page /datasets avec formulaire search et liste dynamique. Utilisez fetch natif pour simplicité.
Page UI Datasets avec search
import { useState, useEffect } from 'react'
export default function DatasetsPage() {
const [datasets, setDatasets] = useState<any[]>([])
const [query, setQuery] = useState('')
const [loading, setLoading] = useState(false)
useEffect(() => {
fetchDatasets()
}, [query])
const fetchDatasets = async () => {
setLoading(true)
const params = new URLSearchParams({ q: query })
const res = await fetch(`/api/datasets?${params}`)
const data = await res.json()
setDatasets(data)
setLoading(false)
}
const addDataset = async (e: React.FormEvent) => {
e.preventDefault()
const form = new FormData(e.target as HTMLFormElement)
await fetch('/api/datasets', {
method: 'POST',
body: JSON.stringify(Object.fromEntries(form)),
})
fetchDatasets()
}
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">Data Catalog</h1>
<form onSubmit={addDataset} className="mb-8 p-4 border rounded">
<input name="name" placeholder="Nom dataset" className="border p-2 mr-2" required />
<input name="description" placeholder="Description" className="border p-2 mr-2" />
<input name="owner" placeholder="Owner" className="border p-2 mr-2" required />
<input name="tags" placeholder="tags,comma,separated" className="border p-2 mr-2" />
<button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">Ajouter</button>
</form>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Rechercher..."
className="w-full border p-4 mb-4 rounded"
/>
{loading ? (
<p>Chargement...</p>
) : (
<div className="grid gap-4">
{datasets.map((ds) => (
<div key={ds.id} className="border p-4 rounded shadow">
<h3 className="font-bold">{ds.name}</h3>
<p>{ds.description}</p>
<p>Owner: {ds.owner} | Tags: {ds.tags.join(', ')}</p>
<pre className="text-xs mt-2">Schema: {JSON.stringify(ds.schema, null, 2)}</pre>
</div>
))}
</div>
)}
</div>
)
}Page React Server/Client mixte avec useEffect pour search live. Formulaire POST basique (ajoutez schema JSON via input hidden si besoin). Tailwind classes pour UI responsive. Fetch natif évite deps externes. Testez sur http://localhost:3000/datasets.
Bonnes pratiques
- Paginatez les queries : Ajoutez
take: 20, skipdans Prisma pour scalabilité. - Authentifiez les APIs : Intégrez NextAuth ou Clerk pour rôles (admin/reader).
- Indexez la DB :
@@index([name])et full-text avectsvectorPostgres pour prod. - Logging et monitoring : Utilisez Sentry pour erreurs API, Prometheus pour metrics.
- Versionnez schémas : Git + Prisma migrate dev vers prod.
Erreurs courantes à éviter
- Connexions DB leak : Toujours singleton Prisma en serverless.
- Pas de validation : Zod/ZodError crash sans parse().body.
- Search non-optimisée : Sans
mode: 'insensitive', résultats incomplets. - Oubli des tags : Array String[] nécessite
tags: { has: q }pour query.
Pour aller plus loin
- Déployez sur Vercel avec Prisma Accelerate pour DB globale.
- Ajoutez lineage avec modèles parents/enfants.
- Explorez Amundsen ou DataHub open-source pour features avancées.
- Découvrez nos formations Learni : Data Engineering avancé pour scaler votre data catalog en enterprise.