A modern full-stack application that lets you upload any Excel file, automatically creates a PostgreSQL table with dynamically inferred schema, detects duplicate uploads, and displays data with powerful sorting, pagination, and search — all without any hardcoded column definitions.
Replace the above links with your actual deployed URLs after completing deployment.
- Drag-and-drop or click-to-browse file upload
- Accepts
.xlsx,.xls, and.csvformats - Client-side Excel parsing using SheetJS (no server needed for parsing)
- Column detection and preview before uploading
- Upload progress indicator with animated progress bar
- Success and error state messages
- Form reset after successful upload
- No hardcoded table schema — columns are detected at runtime
- Automatic PostgreSQL type mapping:
| Sample Values | Inferred Type |
|---|---|
| 1, 42, 100 | INTEGER |
| 12.99, 3.50 | NUMERIC |
| true / yes / false / no | BOOLEAN |
| 2024-01-15 | DATE |
| 2024-01-15T10:00:00Z | TIMESTAMP |
| Anything else | TEXT |
- Duplicate column names auto-deduplicated (
name,name_2,name_3) - Special characters in column headers sanitized to valid SQL identifiers
- SHA-256 hash computed from file content on the frontend
- Backend checks hash against registry before inserting
- On duplicate detection, user is presented a choice:
- Create New Table — always stores a fresh copy
- Update Existing Table — smart row-level merge:
- Identical rows → skipped
- Modified rows (matched by ID column or row hash) → updated
- Brand new rows → inserted
- Merge result shows: rows inserted / updated / skipped
- Fully dynamic column headers from any uploaded file
- Click column headers to sort (ASC / DESC toggle)
- Debounced search across all columns simultaneously
- Pagination with smart page number rendering
- Configurable page size (10 / 20 / 50 / 100 rows)
- Loading overlay while fetching
- Empty state with clear messaging
- Card grid of all previous uploads
- Shows: filename, row count, upload timestamp
- Click any card to instantly load and explore that dataset
- Active table highlighted
- Invalid file type rejected on the frontend before any network call
- Empty Excel file detected and reported
- Inconsistent column counts handled gracefully (missing values →
null) - Backend validation with clear JSON error responses
- SQL injection prevention: all identifiers quoted; table names validated against registry
- Bulk insert with batched
VALUESclauses (500 rows per batch) - All inserts wrapped in a single database transaction
- Paginated queries with
LIMIT/OFFSET - Search uses
ILIKEcast — no full-text index required - 50,000 row upload limit with friendly error message
- 50MB payload limit
| Technology | Purpose |
|---|---|
| React 18 | UI framework |
| TypeScript | Type safety |
| Vite | Build tool & dev server |
| Axios | HTTP client |
| SheetJS (xlsx) | Client-side Excel parsing |
| Custom CSS | Styling (no CSS framework dependency) |
| Technology | Purpose |
|---|---|
| Node.js | Runtime |
| Express.js | Web framework |
| TypeScript | Type safety |
| pg (node-postgres) | Database driver |
| crypto (built-in) | SHA-256 / MD5 hashing |
| dotenv | Environment variable loading |
| Technology | Purpose |
|---|---|
| PostgreSQL | Primary database |
| Neon | Cloud-hosted PostgreSQL |
| Raw SQL | Dynamic CREATE TABLE, INSERT, UPDATE, SELECT |
| Service | Purpose |
|---|---|
| Vercel | Frontend hosting |
| Render | Backend API hosting |
| Neon | Cloud PostgreSQL database |
Frontend (React + Vite + TypeScript) [Vercel]
│
│ Axios — HTTPS API calls
▼
Backend API (Node.js + Express + TypeScript) [Render]
│
│ node-postgres (pg)
▼
PostgreSQL Database [Neon Cloud]
excel-upload-app/
│
├── frontend/ # React Vite application
│ ├── src/
│ │ ├── components/
│ │ │ ├── UploadZone.tsx # Drag-drop upload + hash + duplicate prompt
│ │ │ ├── DataTable.tsx # Dynamic table: sort, search, paginate
│ │ │ └── UploadHistory.tsx # Past upload cards
│ │ ├── utils/
│ │ │ ├── api.ts # Axios API client
│ │ │ └── excelParser.ts # SheetJS Excel → JSON conversion
│ │ ├── types/index.ts # TypeScript interfaces
│ │ ├── App.tsx # Root component with tab navigation
│ │ ├── main.tsx # React entry point
│ │ └── styles.css # Full application CSS
│ ├── index.html
│ ├── vite.config.ts
│ ├── vercel.json # Vercel deployment config
│ ├── .env.example
│ └── package.json
│
├── backend/ # Express API server
│ ├── src/
│ │ ├── controllers/
│ │ │ ├── uploadController.ts # POST /upload, POST /check-hash, POST /merge
│ │ │ └── dataController.ts # GET /data, GET /uploads
│ │ ├── services/
│ │ │ ├── uploadService.ts # Dynamic table creation, bulk insert, merge logic
│ │ │ └── dataService.ts # Paginated + sorted + searched queries
│ │ ├── routes/
│ │ │ └── api.ts # All route definitions
│ │ ├── middleware/
│ │ │ └── errorHandler.ts # Global error handler
│ │ ├── utils/
│ │ │ ├── db.ts # PostgreSQL pool (supports Neon + local)
│ │ │ ├── typeInference.ts # Column type detection logic
│ │ │ └── hashUtils.ts # SHA-256 file hash, MD5 row hash
│ │ └── index.ts # Express app entry point
│ ├── tsconfig.json
│ ├── render.yaml # Render deployment config
│ ├── .env.example
│ └── package.json
│
├── .gitignore
└── README.md
- Node.js 18+
- PostgreSQL 13+ (or a free Neon account for cloud DB)
- npm
git clone https://github.com/YOUR_USERNAME/excel-upload-app.git
cd excel-upload-apppsql -U postgres
CREATE DATABASE excel_upload_db;
\q- Go to neon.tech → Create account → New project
- Copy the Connection String — it looks like:
postgresql://user:password@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require - Use this as
DATABASE_URLin your.env
cd backend
cp .env.example .envEdit .env:
PORT=3001
# Option A — Neon or any PostgreSQL connection string
DATABASE_URL=postgresql://user:password@host/dbname?sslmode=require
# Option B — Individual variables (local PostgreSQL)
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=yourpassword
DB_NAME=excel_upload_db
# Set this AFTER deploying the frontend
FRONTEND_URL=http://localhost:5173npm install
npm run devBackend starts at: http://localhost:3001
Health check: http://localhost:3001/api/health
cd frontend
cp .env.example .envEdit .env:
# During local dev, leave this unset — the Vite proxy handles /api → localhost:3001
# VITE_API_URL=http://localhost:3001/apinpm install
npm run devFrontend starts at: http://localhost:5173
git init
git add .
git commit -m "initial commit: DataGrid Excel Upload App"
git branch -M main
git remote add origin https://github.com/YOUR_USERNAME/excel-upload-app.git
git push -u origin main- Go to neon.tech → Sign up / Log in
- Click New Project → give it a name (e.g.
datagrid-db) - Choose a region closest to your Render backend (e.g.
US East) - Once created, go to Dashboard → Connection Details
- Copy the Connection String (the one with
?sslmode=requireat the end) - Keep this safe — you'll need it for Render
The app auto-creates all tables on first run. No migrations needed.
- Go to render.com → Sign up / Log in
- Click New → Web Service
- Connect your GitHub repository
- Configure:
| Setting | Value |
|---|---|
| Name | datagrid-backend |
| Root Directory | backend |
| Runtime | Node |
| Build Command | npm install && npm run build |
| Start Command | npm start |
- Under Environment Variables, add:
| Key | Value |
|---|---|
NODE_ENV |
production |
PORT |
3001 |
DATABASE_URL |
(paste your Neon connection string) |
FRONTEND_URL |
(leave blank for now — update after Step 4) |
- Click Create Web Service
- Wait for the first deploy to finish (~2-3 minutes)
- Copy your Render URL:
https://datagrid-backend-xxxx.onrender.com
- Go to vercel.com → Sign up / Log in
- Click Add New → Project
- Import your GitHub repository
- Configure:
| Setting | Value |
|---|---|
| Root Directory | frontend |
| Framework Preset | Vite |
| Build Command | npm run build |
| Output Directory | dist |
- Under Environment Variables, add:
| Key | Value |
|---|---|
VITE_API_URL |
https://datagrid-backend-xxxx.onrender.com/api |
- Click Deploy
- Copy your Vercel URL:
https://datagrid-xxxx.vercel.app
- Go back to Render → your backend service → Environment
- Update
FRONTEND_URLto your Vercel URL:https://datagrid-xxxx.vercel.app - Click Save Changes — Render will auto-redeploy
Open your Vercel URL → Upload an Excel file → Verify data appears in the table.
Health check: https://datagrid-backend-xxxx.onrender.com/api/health
| Method | Path | Description |
|---|---|---|
GET |
/api/health |
Health check |
POST |
/api/check-hash |
Check if a file hash already exists |
POST |
/api/upload |
Upload rows → create new table |
POST |
/api/merge |
Merge rows into existing table |
GET |
/api/data |
Fetch table data with filters |
GET |
/api/uploads |
List all past uploads |
Request body:
{ "fileHash": "abc123..." }Response — duplicate found:
{ "exists": true, "tableName": "products_1720000000000" }Response — no duplicate:
{ "exists": false }Request body:
{
"rows": [{ "Name": "Alice", "Age": 30 }, ...],
"fileName": "employees.xlsx",
"fileHash": "abc123..."
}Response:
{
"success": true,
"message": "Successfully uploaded 150 rows.",
"tableName": "employees_1720000000000",
"rowsInserted": 150,
"columns": [
{ "originalName": "Name", "sanitizedName": "name", "type": "TEXT" },
{ "originalName": "Age", "sanitizedName": "age", "type": "INTEGER" }
],
"fileHash": "abc123..."
}Request body:
{
"tableName": "employees_1720000000000",
"rows": [...],
"fileHash": "def456..."
}Response:
{
"success": true,
"message": "Merge complete: 10 inserted, 5 updated, 135 unchanged.",
"tableName": "employees_1720000000000",
"rowsInserted": 10,
"rowsUpdated": 5,
"rowsSkipped": 135
}| Query Param | Type | Default | Description |
|---|---|---|---|
tableName |
string | required | Target table |
page |
number | 1 |
Page number |
pageSize |
number | 20 |
Rows per page (max 200) |
sortColumn |
string | _row_id |
Column to sort by |
sortDirection |
ASC|DESC |
ASC |
Sort direction |
search |
string | — | Search across all columns |
When you upload a file, the backend:
- Receives a JSON array of rows from the frontend
- Reads column names from the first row's keys
- Analyzes all values in each column to infer the best PostgreSQL type
- Sanitizes column names (lowercase, spaces → underscores, deduplicated)
- Generates a unique table name:
<filename>_<timestamp> - Executes
CREATE TABLEwith the inferred schema - Bulk-inserts all rows in batches of 500 inside a single transaction
- Records the upload in
_upload_registry
- Frontend computes a SHA-256 hash of the entire JSON row array
- Sends a
POST /api/check-hashrequest before uploading - Backend queries
_upload_registryfor a matchingfile_hash - If found → returns
{ exists: true, tableName: "..." } - Frontend shows the Create New Table / Update Existing Table prompt
When the user chooses Update Existing Table:
- Backend fetches all existing rows with their
_row_hashand natural ID column - For each incoming row:
- Hash matches exactly → skip (row is identical)
- ID column matches but hash differs → UPDATE all fields (row was modified)
- No match at all → INSERT as a new row
- Returns a count of inserted / updated / skipped rows
- All table names are validated against
_upload_registrybefore any query - All column and table identifiers are wrapped in double quotes:
"column_name" - User input (search terms, sort columns) is passed as parameterized
$1values - Sort column is validated against the actual column list from
information_schema
| Edge Case | How It's Handled |
|---|---|
| Non-Excel file | Rejected on frontend before any API call |
| Empty file | Detected by SheetJS; error shown to user |
| Inconsistent column count | Missing values become null; warning logged |
| Duplicate column names | Sanitizer appends _2, _3 suffix |
| Special characters in headers | Replaced with underscores |
| Headers starting with a digit | Prefixed with _ |
Currency values ($12.99) |
$ and , stripped; stored as NUMERIC |
| Large files (>50,000 rows) | Rejected with friendly message |
| Failed transaction | Full ROLLBACK; no partial data stored |
| Null / empty cells | Coerced to null safely |
- Docker Compose for one-command local setup
- GitHub Actions CI/CD pipeline
- Column type override UI before upload
- Export table back to Excel / CSV
- Multi-sheet Excel file support
- Chart / visualization tab for numeric columns
- User authentication (JWT)
- Per-user isolated datasets
- Table deletion from UI
- Row-level edit and delete
- Webhook notifications on upload
- Redis caching for repeated queries
Contributions, suggestions, and bug reports are welcome.
# Fork → Clone → Branch → Commit → Push → Pull Request
git checkout -b feature/your-feature-name
git commit -m "feat: describe your change"
git push origin feature/your-feature-nameThis project is licensed under the MIT License.
Developed by Yogeshwaran S
https://github.com/YOUR_USERNAME/excel-upload-app
If you found this project useful:
- ⭐ Star the repository
- 🍴 Fork the project
- 🐛 Report bugs via Issues
- 💡 Suggest features via Discussions
✅ Active Development ✅ Production Deployed ✅ Dynamic Schema Inference ✅ Duplicate File Detection ✅ Smart Row-Level Merge ✅ Full Stack TypeScript ✅ Neon Cloud Database Connected ✅ Production Ready Architecture