Loading React Diagram from PostgreSQL database
The React Diagram component allows visualization of organizational chart layout using data from a PostgreSQL database through a REST API. This guide explains how to set up the PostgreSQL database, create a Node.js backend service, and link the data to the React Diagram component to display an organizational chart layout.
Note: This guide works with React Diagram version 28.x or later. The REST API needs to return an array of JSON objects with id, parent_id, and role fields for correct data binding.
Overview
This integration workflow connects:
- PostgreSQL – Stores the organizational chart data.
- Node.js + Express – REST API that serves JSON data from PostgreSQL.
- React + EJ2 Diagram – data binding via DataManager and rendering of an organizational chart layout.
Prerequisites
Ensure availability of the following software:
- Node.js 20.x or newer
- PostgreSQL 12.x or newer
- TypeScript 5.x
- React 19.x or newer
PostgreSQL database setup
Installing PostgreSQL
Download PostgreSQL from the official website: https://www.postgresql.org/download/
Installation Steps:
- Download the installer for the preferred version (12.x or higher recommended)
- Run the installer and follow the setup wizard
- During installation:
- Set a password for the PostgreSQL (example: postgres123) and remember it.
- Keep the default port 5432.
- Next, the Select Components screen will open.
- By default, all options are selected, as shown in the image:
- Uncheck the Stack Builder option — it is not necessary for this setup.
- Ensure PostgreSQL Server, pgAdmin 4, and Command Line Tools are selected.
- Complete the installation.
Two options are available to create a database:
- Manual (pgAdmin 4)
- Automated initialization and seeding (seed script).
Option A: Manual (pgAdmin 4)
Opening pgAdmin
PostgreSQL includes pgAdmin 4, a graphical tool for database management. Open pgAdmin 4 from the Windows Start menu or application launcher.

Creating the database
Right-click on Databases option and select Create → Database.

In the Create - Database dialog:
- Enter org_chart_db as the database name.
- Click Save to create the database.

After creating the database, right-click the org_chart_db database and choose Query Tool from the context menu.
Quick procedure before running SQL:
- Clear the editor (Ctrl+A → Delete) to remove any previous statements.
- Enter the SQL, then click Execute / Execute Query (or press F5) to run it.
- After execution, clear the editor again before entering the next statement.
Follow this simple sequence for every SQL in this guide.
Creating the table
Run the following SQL to create the org_chart_layout table:
CREATE TABLE IF NOT EXISTS org_chart_layout (
id text PRIMARY KEY,
role text NOT NULL,
parent_id text NULL
);
The table structure includes:
- id - Primary key for unique node identification.
- role - Display text for the node in the organizational chart layout.
- parent_id - Foreign key reference to the parent node (NULL for root).
Inserting sample data
Add organizational chart data using the SQL INSERT statement. The sample data shows a typical organizational structure with board, management, and department levels.
INSERT INTO org_chart_layout (id, role, parent_id) VALUES
('parent', 'Board', NULL),
('1', 'General Manager', 'parent'),
('2', 'Human Resource Manager', '1'),
('3', 'Trainers', '2'),
('4', 'Recruiting Team', '2'),
('5', 'Finance Asst. Manager', '2'),
('6', 'Design Manager', '1'),
('7', 'Design Supervisor', '6'),
('8', 'Development Supervisor', '6'),
('9', 'Drafting Supervisor', '6'),
('10', 'Operations Manager', '1'),
('11', 'Statistics Department', '10'),
('12', 'Logistics Department', '10'),
('16', 'Marketing Manager', '1'),
('17', 'Overseas Sales Manager', '16'),
('18', 'Petroleum Manager', '16'),
('20', 'Service Department Manager', '16'),
('21', 'Quality Control Department', '16')
ON CONFLICT (id) DO UPDATE
SET role = EXCLUDED.role,
parent_id = EXCLUDED.parent_id;
Verifying data insertion
Run a SELECT query to confirm the data insertion:
SELECT * FROM org_chart_layout ORDER BY id;The query should return the inserted rows. Parent–child relationships are indicated by the parent_id column, which references the id of the parent node (NULL for root nodes).

Option B: Automated (seed script)
This project includes an automated initialization script that handles database creation, table schema generation, and data seeding in one command.
The script performs the following operations:
- Dynamic Database Provisioning: Detects if the database exists and creates it automatically.
- Schema Generation: Creates the org_chart_layout table with the required primary keys and organizational relationships.
- Data Seeding: Populates the table with organizational data from a JSON source.
The implementation details for the automated initialization script are covered in the Automated database initialization and seeding section under Backend Implementation.
Backend implementation
Backend dependencies
Create a server folder inside the project root folder, navigate to it, and install the following packages:
cd server
npm install express pg cors dotenv
npm install -D @types/express @types/pg @types/cors @types/node typescript tsx nodemonDependencies Explained:
- express - Web framework for building REST APIs.
- pg - PostgreSQL client for Node.js.
- cors - To able cross-origin requests.
- dot env - Loads environment variables from the .env file.
- typescript, tsx - TypeScript runtime and compiler.
Database connection configuration
Environment configuration
Create a server/.env file with the following configuration:
DB_USER=postgres
DB_PASSWORD=your_password
DB_HOST=localhost
DB_PORT=5432
DB_NAME=org_chart_db
PORT=5000
NODE_ENV=developmentNote: Update DB_PASSWORD to match your local PostgreSQL credentials.
Connection pool configuration
Create a server/src/db/index.ts file to configure the connection pool that manages database connections efficiently.
import { Pool } from 'pg';
import dotenv from 'dotenv';
dotenv.config();
const pool = new Pool({
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
pool.on('error', (err) => {
console.error('Unexpected error on idle client', err);
process.exit(-1);
});
export default pool;The error handler captures unexpected database errors on idle connections and terminates the application to prevent operating in an unstable state.
Important: Replace your_password with the actual PostgreSQL password set during installation. Never commit the .env file to version control.
TypeScript type definitions
Create a server/src/types/layout.types.ts file and add the following interface:
export interface LayoutNode {
id: string;
parent_id: string | null;
role: string;
}Automated database initialization and seeding
This section explains the automated database initialization and seeding process that creates the database, applies the schema, and populates the initial organizational chart data.
Seed data file
Create a server/src/data/layoutSeed.json file with the organizational chart data:
[
{ "id": "parent", "parent_id": null, "role": "Board" },
{ "id": "1", "parent_id": "parent", "role": "General Manager" },
{ "id": "2", "parent_id": "1", "role": "Human Resource Manager" }
// More data...
]Seed script
Create a server/src/script/seedLayout.ts file that automates database creation and data insertion:
import pool from '../db/index.js';
import pkg from 'pg';
const { Client } = pkg;
import { readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { LayoutNode } from '../types/layout.types.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const seedDatabase = async (): Promise<void> => {
const dbName = process.env.DB_NAME || 'org_chart_db';
// Step 1: Create database if it doesn't exist
const systemClient = new Client({
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'),
database: 'postgres',
});
try {
await systemClient.connect();
const res = await systemClient.query(
`SELECT 1 FROM pg_database WHERE datname = '${dbName}'`
);
if (res.rowCount === 0) {
console.log(`Database "${dbName}" not found. Creating...`);
await systemClient.query(`CREATE DATABASE ${dbName}`);
console.log(`Database "${dbName}" created successfully.`);
} else {
console.log(`Database "${dbName}" already exists.`);
}
} catch (error) {
console.error('Error checking/creating database:', error);
} finally {
await systemClient.end();
}
const client = await pool.connect();
try {
console.log('Starting database seeding...');
const seedDataPath = join(__dirname, '../data/layoutSeed.json');
const seedData: LayoutNode[] = JSON.parse(readFileSync(seedDataPath, 'utf-8'));
await client.query(`
DROP TABLE IF EXISTS org_chart_layout;
CREATE TABLE org_chart_layout (
id TEXT PRIMARY KEY,
parent_id TEXT,
role TEXT NOT NULL,
FOREIGN KEY (parent_id) REFERENCES org_chart_layout(id) ON DELETE CASCADE
);
CREATE INDEX idx_org_chart_layout_parent_id ON org_chart_layout(parent_id);
`);
console.log('Table created successfully');
for (const node of seedData) {
await client.query(
'INSERT INTO org_chart_layout (id, parent_id, role) VALUES ($1, $2, $3)',
[node.id, node.parent_id, node.role]
);
}
console.log(`Successfully seeded ${seedData.length} nodes`);
console.log('Database seeding completed!');
process.exit(0);
} catch (error) {
console.error('Error seeding database:', error);
process.exit(1);
} finally {
client.release();
}
};
seedDatabase();Running the seed script
Add the seed script to your server/package.json file:
{
"scripts": {
"build": "tsc",
"start": "node dist/server.js",
"dev": "tsx watch src/server.ts",
"seed": "tsx src/script/seedLayout.ts"
}
}Execute the following command to initialize the database:
cd server
npm run seedThis creates the database, table schema, and populates it with organizational chart nodes.
Backend API endpoints
Create the API layer to expose organizational chart data to the frontend.
Controller
Create a server/src/controllers/layout.controller.ts file to handle database queries:
import { Request, Response } from 'express';
import pool from '../db/index.js';
import { LayoutNode } from '../types/layout.types.js';
/**
* Fetches all organizational chart layout nodes from the database
* Returns organizational chart data as JSON array
*/
export const getLayoutData = async (req: Request, res: Response): Promise<void> => {
try {
// Execute SQL query to fetch all nodes ordered by id
const result = await pool.query<LayoutNode>(
'SELECT id, parent_id, role FROM org_chart_layout ORDER BY id'
);
// Send the rows array as JSON response
res.json(result.rows);
} catch (error) {
// Log error for debugging
console.error('Error fetching layout data:', error);
// Extract error message safely
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
// Return 500 Internal Server Error with details in development mode
res.status(500).json({
message: 'Failed to fetch layout data',
error: process.env.NODE_ENV === 'development' ? errorMessage : undefined
});
}
};Routes
Create a server/src/routes/layout.routes.ts file to map URL endpoints to controller functions:
import { Router } from 'express';
import { getLayoutData } from '../controllers/layout.controller.js';
// Create a new router instance
const router = Router();
// Define GET endpoint: /api/layout
// When accessed, it calls the `getLayoutData` controller function
router.get('/layout', getLayoutData);
export default router;This creates the endpoint http://localhost:5000/api/layout that returns the organizational chart data.
Main server file
Create a server/src/server.ts file to configure Express, enable CORS, and start the server:
import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import pool from './db/index.js';
import layoutRoutes from './routes/layout.routes.js';
// Load environment variables
dotenv.config();
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware: Parse incoming JSON request bodies
app.use(express.json());
// Middleware: Enable CORS for React frontend
// Allows requests from Vite dev server (ports 5173, 5174)
app.use(cors({
origin: ['http://localhost:5173', 'http://localhost:5174'],
credentials: true
}));
// Register API routes with /api prefix
// All routes from `layoutRoutes` will be accessible at /api/*
app.use('/api', layoutRoutes);
// 404 Handler: Catch all undefined routes
app.use((req: Request, res: Response) => {
res.status(404).json({ message: 'Route not found' });
});
// Global Error Handler: Catch any unhandled errors
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error('Server error:', err);
res.status(500).json({
message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
/**
* Start the Express server
* First tests database connectivity, then starts HTTP server
*/
const startServer = async (): Promise<void> => {
try {
// Test database connection before starting server
await pool.query('SELECT NOW()');
console.log('✓ Database connected successfully');
// Start listening for HTTP requests
app.listen(PORT, () => {
console.log(`✓ Server running on http://localhost:${PORT}`);
console.log(`✓ API endpoint: http://localhost:${PORT}/api/layout`);
});
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1); // Exit with error code
}
};
startServer();Running the backend server
Open a terminal and navigate to the server folder:
cd server
npm run devExpected console output when the server starts successfully:
✓ Database connected successfully
✓ Server running on http://localhost:5000
✓ API endpoint: http://localhost:5000/api/layout
This confirms:
- PostgreSQL connection is established.
- Express server is listening on port 5000.
- The API endpoint is accessible.
Important: Keep this terminal window open. The backend must continue running to serve data to the frontend.
Frontend implementation
Frontend dependencies
From your project root, scaffold the client application using Vite (React + TypeScript template):
npm create vite@latest client -- --template react-tsThen install the UI and type dependencies:
cd client
npm install @syncfusion/ej2-react-diagrams @syncfusion/ej2-data @syncfusion/ej2-base
npm install react react-dom
npm install -D @types/react @types/react-dom typescript vitePackage Overview:
- @syncfusion/ej2-react-diagrams - React Diagram component with OrganizationalChart layout support.
- @syncfusion/ej2-data - DataManager for data binding.
- @syncfusion/ej2-base - Core utilities and base components.
- vite - Fast build tool and development server.
Import Syncfusion® CSS styles in the client/src/index.css file for proper component rendering:
/* Import Material theme base styles */
@import '@syncfusion/ej2-base/styles/material.css';
/* Import Diagram component-specific styles */
@import '@syncfusion/ej2-react-diagrams/styles/material.css';Note: Syncfusion® provides multiple themes (Material, Bootstrap, Fabric). This example uses Material theme for modern appearance.
TypeScript type definitions
Create a client/src/types/layout.types.ts file and add the following interface:
export interface LayoutNode {
id: string;
parent_id: string | null;
role: string;
}Service layer
Create a client/src/services/layoutService.ts file to handle API communication:
This service layer handles the HTTP request, performs error checking, and returns typed data that components can use.
import type { LayoutNode } from '../types/layout.types';
const API_URL = 'http://localhost:5000/api/layout';
export const fetchLayoutData = async (): Promise<LayoutNode[]> => {
const response = await fetch(API_URL);
if (!response.ok) {
throw new Error(`Failed to fetch layout data: ${response.statusText}`);
}
return response.json();
};Diagram component
Create a OrganizationalLayout.tsx file inside client/src/components/ folder to render the organizational chart layout:
The Diagram component fetches data on mount, binds it to the DataManager, and configures organizational chart layout properties.
import { useEffect, useState } from 'react';
import {
DiagramComponent,
Inject,
HierarchicalTree,
DataBinding,
Node,
Connector
} from '@syncfusion/ej2-react-diagrams';
import { DataManager } from '@syncfusion/ej2-data';
import type { LayoutNode } from '../types/layout.types';
import { fetchLayoutData } from '../services/layoutService';
const OrganizationalLayout = () => {
// State management for data, loading, and error states
const [data, setData] = useState<LayoutNode[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
// Fetch data when component mounts
useEffect(() => {
const loadData = async () => {
try {
setLoading(true);
// Call service layer to fetch data from backend
const layoutData = await fetchLayoutData();
setData(layoutData);
setError(null);
} catch (err) {
// Handle fetch errors gracefully
const errorMessage = err instanceof Error
? err.message
: 'Failed to load diagram data';
setError(errorMessage);
console.error('Error loading layout data:', err);
} finally {
setLoading(false);
}
};
loadData();
}, []); // Empty dependency array - runs once on mount
// Show loading message while fetching data
if (loading) {
return <div>Loading diagram...</div>;
}
// Show error message if fetch failed
if (error) {
return (
<div>
<h3>Error Loading Diagram</h3>
<p>{error}</p>
</div>
);
}
// Render diagram once data is loaded
return (
<DiagramComponent
id="diagram"
width="100%"
height="700px"
// Data binding configuration
dataSourceSettings=
// Layout algorithm configuration
layout=
// Customize default node appearance
getNodeDefaults={(node: Node) => {
node.width = 100;
node.height = 40;
node.shape = { type: 'Basic', shape: 'Rectangle' };
node.style = { fill: '#6BA5D7', strokeColor: '#6BA5D7' };
// Add text annotation showing the role field
node.annotations = [{
content: (node.data as LayoutNode).role,
style: { color: 'white' }
}];
return node;
}}
// Customize default connector appearance
getConnectorDefaults={(connector: Connector) => {
connector.type = 'Orthogonal'; // 90-degree angle connectors
connector.targetDecorator = {
shape: 'Arrow',
width: 10,
height: 10
};
connector.style = { strokeColor: '#6BA5D7' };
return connector;
}}
>
<Inject services={[HierarchicalTree, DataBinding]} />
</DiagramComponent>
);
};
export default OrganizationalLayout;Add the OrganizationalLayout component to App.tsx as shown below.
import './App.css';
import OrganizationalLayout from './components/OrganizationalLayout';
function App() {
return (
<div className="app-container">
<header className="app-header">
<h1>React Diagram - Organizational Chart Layout</h1>
</header>
<main className="app-main">
<OrganizationalLayout />
</main>
</div>
);
}
export default App;Running the application
Run the client dev server:
cd client
npm run devOpen http://localhost:5173 in a browser. The organizational chart should render using data fetched from the backend API.
Application Flow:
- React component mounts → triggers useEffect hook.
- Service layer fetches data from http://localhost:5000/api/layout.
- Backend queries PostgreSQL and returns JSON array.
- Frontend receives data → wraps it with DataManager.
- DiagramComponent renders organizational chart visualization.
Note: If the diagram doesn’t appear, press F12 to check the browser console for errors and verify both backend and frontend servers are running.
Troubleshooting
Database initialization issues
Symptom: Error: database "org_chart_db" does not exist
Root Cause: The initialization script has not been run, or the database user lacks creation privileges.
Resolution Steps:
- Run Initialization: Execute npm run seed in the server folder.
-
PostgreSQL Service: Ensure PostgreSQL is running on your system:
Get-Service -Name postgresql* - Check Permissions: Ensure the database user has CREATEDB rights.
- Environment Check: Verify DB_NAME in server/.env matches the expected name.
Symptom: Error: password authentication failed
Root Cause: The password in server/.env does not match the PostgreSQL user password.
Resolution Steps:
- Open pgAdmin 4.
- Right-click on user → Properties → Definition.
- Enter a new password and save.
- Update DB_PASSWORD in server/.env with the new password.
- Restart the Node.js server with npm run dev.
Tip: Avoid special characters in PostgreSQL passwords to prevent shell escaping issues.
CORS configuration
CORS (Cross-Origin Resource Sharing) errors occur when the frontend and backend run on different ports without proper permission configuration.
Symptom: Browser console shows Access to fetch at 'http://localhost:5000/api/layout' from origin 'http://localhost:5173' has been blocked by CORS policy
Root Cause: The backend CORS configuration does not include the frontend’s port.
Resolution Steps:
- Check the frontend port (usually 5173).
- Open server/src/server.ts.
- Update the CORS origin array to include all frontend ports:
app.use(cors({ origin: ['http://localhost:5173', 'http://localhost:5174', 'http://localhost:3000'], credentials: true })); - Save the file and refresh the browser page.
Additional Check: If using a different port, add it to the origins array.
Empty diagram rendering
An empty diagram (no nodes visible) can result from API failures, empty database, or data binding issues.
Symptom: Page loads successfully but diagram area is blank with no nodes displayed.
Diagnostic Steps:
-
Verify API is responding:
Invoke-WebRequest -Uri http://localhost:5000/api/layout -UseBasicParsingExpected: JSON array with organizational chart data should be returned.
-
Check database has data:
Open pgAdmin Query Tool and execute:SELECT COUNT(*) FROM org_chart_layout;Expected: Should return the number of records inserted.
-
Inspect browser Network tab:
- Press F12 → Network tab.
- Refresh the page.
- Look for the layout request.
- Status should be 200 OK.
- Preview tab should show JSON data.
-
Check component state:
- Add console logs in
OrganizationalLayout.tsx:useEffect(() => { const loadData = async () => { const layoutData = await fetchLayoutData(); // Log data console.log('Fetched data:', layoutData); setData(layoutData); }; loadData(); }, []); - Verify data is being received and set correctly.
- Add console logs in
Common Resolution: Most empty diagram issues are caused by the backend server not running or CORS blocking the API request.
Please find the sample in this Github location