Introduction
A CMDB (Configuration Management Database) is the heart of ITIL management: it stores Configuration Items (CIs) like servers, applications, or networks, and their relationships (dependencies, hosting). Without it, tracking incident impacts becomes chaotic—like navigating a data center without a map.
This beginner tutorial shows you how to build a fully functional CMDB in Node.js with Express (REST API) and Prisma (ORM on SQLite). Why this stack? SQLite for zero DB config (perfect for local dev), Prisma for type-safe schemas, Express for quick routes. By the end, you'll have a complete CRUD API: list, create, and link CIs. Real-world example: model a web server hosting an app, with automated relationships.
Pro tip: this base scales to PostgreSQL. Estimated time: 30 min. Value: bookmark for your ITIL audits.
Prerequisites
- Node.js 20+ installed (download here)
- Basic JavaScript knowledge (variables, async functions)
- Code editor (VS Code recommended with Prisma extension)
- Terminal (PowerShell or bash)
- 5 min for setup
Initialize the project and install dependencies
mkdir cmdb-simple
cd cmdb-simple
npm init -y
npm install express @prisma/client prisma typescript ts-node @types/express @types/node
npm install -D @types/node
npx prisma init --datasource-provider sqliteThis command creates the project folder, initializes package.json, installs Express for the API, Prisma for the ORM, and TypeScript for typing. Prisma init generates prisma/schema.prisma and .env with DATABASE_URL for SQLite. Run it all at once: your setup is ready in 1 min, no version pitfalls.
Understanding the CMDB schema
Before the code, let's conceptualize: a CMDB has CIs (e.g., 'Serveur-Web-01', type 'Server') and Relations (e.g., 'hosts' to 'App-Ecommerce'). Analogy: CIs are nodes, relations are edges in a graph. We use two Prisma models linked by foreign keys to avoid duplicates and ensure integrity.
Define Prisma models for CIs and Relations
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model CI {
id String @id @default(cuid())
name String @unique
type String // ex: Serveur, Application, Reseau
description String?
relationsAsFrom Relation[] @relation("FromTo")
relationsAsTo Relation[] @relation("ToFrom")
}
model Relation {
id String @id @default(cuid())
type String // ex: heberge, depend, connecte
ciFromId String
ciToId String
ciFrom CI @relation("FromTo", fields: [ciFromId], references: [id], onDelete: Cascade)
ciTo CI @relation("ToFrom", fields: [ciToId], references: [id], onDelete: Cascade)
@@unique([ciFromId, ciToId, type])
}This schema defines CIs with unique names and types, plus bidirectional Relations for the graph. @@unique prevents duplicate relations. Cascade auto-deletes orphans. Pitfall: without onDelete, deletions fail; test with npx prisma db push after edits.
Apply the migration and generate the Prisma client
npx prisma db push
npx prisma generatedb push creates the SQLite DB without formal migrations (ideal for prototypes). generate compiles the TS client. Result: ./prisma/dev.db exists with tables. Verify with sqlite3 prisma/dev.db ".schema" – no errors if good.
Create the Express server with TypeScript typing
Now, the API: REST endpoints for CIs (list, create) and relations. We use async/await for clarity, Prisma for type-safe queries. Port 3001 to avoid conflicts.
Implement the main server and CI routes
import express from 'express';
import { PrismaClient } from '@prisma/client';
const app = express();
const prisma = new PrismaClient();
app.use(express.json());
const PORT = 3001;
// GET /cis - List all CIs with relations
app.get('/cis', async (req, res) => {
try {
const cis = await prisma.cI.findMany({
include: {
relationsAsFrom: true,
relationsAsTo: true,
},
});
res.json(cis);
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
// POST /ci - Create a CI
app.post('/ci', async (req, res) => {
try {
const { name, type, description } = req.body;
const ci = await prisma.cI.create({
data: { name, type, description },
});
res.json(ci);
} catch (error) {
res.status(400).json({ error: 'CI exists or invalid data' });
}
});
app.listen(PORT, () => {
console.log(`CMDB server on http://localhost:${PORT}`);
});This server exposes GET /cis (with include for full graph) and POST /ci. Try/catch handles Prisma errors (e.g., unique violations). Run with npx ts-node server.ts – curl http://localhost:3001/cis is empty at first. Pitfall: forget express.json() and POST body won't parse.
Add routes for Relations
// Add these routes to server.ts after the CI routes
// POST /relation - Create a relation
app.post('/relation', async (req, res) => {
try {
const { type, ciFromId, ciToId } = req.body;
// Check CI existence
const ciFrom = await prisma.cI.findUnique({ where: { id: ciFromId } });
const ciTo = await prisma.cI.findUnique({ where: { id: ciToId } });
if (!ciFrom || !ciTo) {
return res.status(404).json({ error: 'CI not found' });
}
const relation = await prisma.relation.create({
data: { type, ciFromId, ciToId },
include: { ciFrom: true, ciTo: true },
});
res.json(relation);
} catch (error) {
res.status(400).json({ error: 'Duplicate or invalid relation' });
}
});
// DELETE /relation/:id
app.delete('/relation/:id', async (req, res) => {
try {
await prisma.relation.delete({ where: { id: req.params.id } });
res.json({ message: 'Relation deleted' });
} catch (error) {
res.status(404).json({ error: 'Relation not found' });
}
});Paste these routes into server.ts. POST checks existing CIs before creation (integrity). Include returns linked CIs. DELETE by ID. Test: create 2 CIs, add relation; GET /cis shows the graph. Pitfall: without checks, foreign key errors crash.
Seeding script for sample data
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// Create example CIs
const serveur = await prisma.cI.create({
data: { name: 'Serveur-Web-01', type: 'Serveur', description: 'Nginx sur Ubuntu' },
});
const app = await prisma.cI.create({
data: { name: 'App-Ecommerce', type: 'Application', description: 'React + Node' },
});
const bd = await prisma.cI.create({
data: { name: 'DB-Prod-01', type: 'Base de Données', description: 'PostgreSQL' },
});
// Create relations
await prisma.relation.create({
data: { type: 'heberge', ciFromId: serveur.id, ciToId: app.id },
});
await prisma.relation.create({
data: { type: 'depend', ciFromId: app.id, ciToId: bd.id },
});
console.log('Seed OK: 3 CI, 2 relations');
}
main()
.finally(async () => {
await prisma.$disconnect();
});This script populates the DB with realistic examples (server → app → DB). Run npx ts-node seed.ts before the API. Verify: curl GET /cis shows nested objects. Pitfall: forget $disconnect, connections leak in dev.
Package.json with ready-to-use scripts
{
"name": "cmdb-simple",
"version": "1.0.0",
"scripts": {
"dev": "ts-node server.ts",
"seed": "ts-node seed.ts",
"db:push": "prisma db push",
"generate": "prisma generate"
},
"dependencies": {
"express": "^4.19.2",
"@prisma/client": "^5.14.0",
"prisma": "^5.14.0",
"typescript": "^5.5.3",
"ts-node": "^10.9.2",
"@types/express": "^4.17.21",
"@types/node": "^22.0.0"
}
}Copy this complete package.json (replace the auto-generated one). npm run dev starts the server, run seed populates. Versions pinned for 2026 reproducibility. Pitfall: missing deps break ts-node.
Test your CMDB
- Run
npm run seed npm run dev- curl -X POST http://localhost:3001/ci -H "Content-Type: application/json" -d '{"name":"Reseau-01","type":"Reseau"}'
- curl http://localhost:3001/cis (see the graph)
- curl -X POST http://localhost:3001/relation -H "Content-Type: application/json" -d '{"type":"connecte","ciFromId":"
","ciToId":" "}'
Best practices
- Input validation: Add Zod for schemas (npm i zod) – e.g., z.object({name: z.string().min(1)})
- Pagination: On GET /cis, add skip/take for >100 CIs
- Auth: Integrate JWT (npm i jsonwebtoken) on sensitive routes
- Logs: Use Winston to trace Prisma queries
- Backup: Cron script copying prisma/dev.db
Common errors to avoid
- Forget include: GET /cis without relations shows isolated CIs – always nest for graph view
- Manual migrations: db push OK for dev, but use migrate deploy in prod
- Connection leaks: Always prisma.$disconnect() in scripts
- CORS: Add app.use(cors()) for remote frontends
- Unique violations: Test CI names unique before POST
Next steps
- Migrate to PostgreSQL: swap URL for prod DB
- Visualize graph: Integrate Cytoscape.js frontend
- Advanced ITIL: Add Status, Owner fields
- Pro tools: ServiceNow API or open-source iTop