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.
⚙️ 1. Setting Up the Backend (NestJS + GraphQL + MongoDB)
Start by creating a new NestJS project:
npm i -g @nestjs/cli
nest new excel-share-api
Then install the dependencies:
npm install @nestjs/graphql @nestjs/apollo graphql apollo-server-express mongoose @nestjs/mongoose
In 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 {}
🧩 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;
}
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.
💻 3. Building the Frontend (React + Handsontable + Apollo Client)
In your React app:
npx create-react-app excel-share-frontend
npm install handsontable @handsontable/react @apollo/client graphql
Then, create a simple Handsontable grid connected to GraphQL:
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 (
{data?.sheets[0]?.name}
data={sheetData}
colHeaders={true}
rowHeaders={true}
afterChange={handleChange}
licenseKey="non-commercial-and-evaluation"
/>
);
}
export default ExcelSheet;
Now you have a live spreadsheet UI that syncs changes through GraphQL to MongoDB.
⚡ 4. Adding Real-Time Collaboration (Optional)
To make it collaborative, you can integrate GraphQL subscriptions or WebSockets in NestJS.
Each time a user updates a cell, emit a message to other clients to refresh the grid.
Example setup:
Use @nestjs/websockets for socket updates.
Broadcast updates 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 structures.
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 enterprise systems with React, NestJS, GraphQL, and MongoDB.