I’ve always loved the flexibility of Excel — but when teams need to collaborate in real time, spreadsheets can quickly become a mess of conflicting versions and broken formulas.
So I decided to build something different: a web-based collaborative spreadsheet app, where multiple users can view and edit data directly from the browser — powered by React, Handsontable, NestJS, GraphQL, and MongoDB.
This article walks you through the architecture, setup, and lessons learned from building this project.
💡 The Goal
The idea was simple:
- A web interface that feels like Excel
- A backend API to handle CRUD operations
- A database to persist sheet data
- Real-time synchronization between users
🧱 The Tech Stack
Here’s what I chose and why:
- React + Handsontable → To create a dynamic, spreadsheet-like UI
- NestJS → For a structured, TypeScript-based backend
- GraphQL → To efficiently fetch and mutate sheet data
- MongoDB → To store sheet contents and metadata flexibly
This combination gives both speed and scalability — perfect for small SaaS apps or internal tools.
⚙️ Step 1: Setting Up the Backend (NestJS + GraphQL + MongoDB)
Create a new NestJS project:
npm i -g @nestjs/cli
nest new excel-share-api
Install dependencies:
npm install @nestjs/graphql @nestjs/apollo graphql apollo-server-express mongoose @nestjs/mongoose
app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { MongooseModule } from '@nestjs/mongoose';
import { SheetModule } from './sheet/sheet.module';
@Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: true,
playground: true,
}),
MongooseModule.forRoot('mongodb://localhost:27017/excel_share'),
SheetModule,
],
})
export class AppModule {}
🧩 Step 2: Create a Sheet Schema and Resolver
sheet.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
@Schema()
export class Sheet extends Document {
@Prop({ required: true })
name: string;
@Prop({ type: Object })
data: Record<string, any>;
}
export const SheetSchema = SchemaFactory.createForClass(Sheet);
sheet.resolver.ts
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { SheetService } from './sheet.service';
import { Sheet } from './sheet.model';
@Resolver(() => Sheet)
export class SheetResolver {
constructor(private sheetService: SheetService) {}
@Query(() => [Sheet])
async sheets() {
return this.sheetService.findAll();
}
@Mutation(() => Sheet)
async updateSheet(
@Args('id') id: string,
@Args('data') data: string
) {
return this.sheetService.update(id, JSON.parse(data));
}
}
Now your backend is ready to query and update spreadsheet data in MongoDB through GraphQL.
💻 Step 3: Building the Frontend (React + Handsontable + Apollo Client)
Create the React app and install dependencies:
npx create-react-app excel-share-frontend
npm install handsontable @handsontable/react @apollo/client graphql
ExcelSheet.js
import React, { useState, useEffect } from 'react';
import { HotTable } from '@handsontable/react';
import { useQuery, useMutation, gql } from '@apollo/client';
const GET_SHEET = gql`
query {
sheets {
_id
name
data
}
}
`;
const UPDATE_SHEET = gql`
mutation UpdateSheet($id: String!, $data: String!) {
updateSheet(id: $id, data: $data) {
_id
name
data
}
}
`;
function ExcelSheet() {
const { data } = useQuery(GET_SHEET);
const [updateSheet] = useMutation(UPDATE_SHEET);
const [sheetData, setSheetData] = useState([]);
useEffect(() => {
if (data && data.sheets.length > 0) {
setSheetData(data.sheets[0].data);
}
}, [data]);
const handleChange = async (changes, source) => {
if (source === 'loadData') return;
await updateSheet({
variables: {
id: data.sheets[0]._id,
data: JSON.stringify(sheetData),
},
});
};
return (
<div>
<h2>{data?.sheets[0]?.name}</h2>
<HotTable
data={sheetData}
colHeaders={true}
rowHeaders={true}
afterChange={handleChange}
licenseKey="non-commercial-and-evaluation"
/>
</div>
);
}
export default ExcelSheet;
Now you have a live spreadsheet UI that syncs changes through GraphQL to MongoDB.
⚡ Step 4: Adding Real-Time Collaboration (Optional)
To make it collaborative, integrate GraphQL Subscriptions or WebSockets in NestJS.
Each time a user updates a cell, broadcast that update to all connected clients.
Example setup:
- Use
@nestjs/websockets
for socket updates - Broadcast using Socket.io
- On the frontend, listen for events and refresh the table
🧠 Lessons Learned
- Handsontable is powerful — but you must handle performance when data grows.
- GraphQL fits perfectly for partial data updates and flexible queries.
- MongoDB’s document model makes it easy to store dynamic sheet data.
- Clear data versioning and change tracking are critical for collaboration.
🚀 Final Thoughts
Building an Excel-like collaboration tool is challenging but rewarding.
With React, NestJS, GraphQL, and MongoDB, you can achieve both flexibility and performance — and Handsontable gives your users the familiar spreadsheet experience they love.
This setup can easily become the foundation for:
- Internal business tools
- Data analysis dashboards
- Lightweight SaaS spreadsheet applications
All running with one unified stack — TypeScript end to end.
👨💻 Written by Steven Nguyen — Engineering Manager | Building scalable enterprise systems with Java, NestJS, and modern DevOps practices.
Top comments (1)
Step by step approach to creating a flexible, scalable spreadsheet app with popular JS technologies. Highlights include schema design, UI binding, and strategies for live collaboration.
Great reference for developers building interactive enterprise tools.