A minimal, fast note-taking application built on the MERN stack. ThinkBoard provides a clean interface for creating, editing, and organising short notes, backed by a REST API with distributed rate limiting.
Live at: https://thinkboardpro.onrender.com/
- Live Demo
- Features
- Tech Stack
- Project Structure
- Prerequisites
- Getting Started
- Environment Variables
- API Reference
- Production Build
- Security Considerations
- Roadmap
- Contributing
- License
A hosted instance is available at https://thinkboardpro.onrender.com/.
The demo runs on Render's free tier, so the first request after a period of inactivity may take a few seconds while the service wakes up. Notes created there are public — see Security Considerations.
- Full CRUD for notes — create, read, update, and delete notes through a REST API.
- Responsive UI — built with React 19, Tailwind CSS v4, and daisyUI; adapts from mobile to widescreen.
- Distributed rate limiting — Upstash Redis sliding-window limiter protects the API from request floods.
- Graceful degradation — dedicated UI states for loading, empty results, and rate-limited responses.
- Toast notifications — immediate feedback on every mutation via
react-hot-toast. - Single-service deployment — in production the Express server also serves the compiled frontend, so the whole app runs as one process.
| Layer | Technology |
|---|---|
| Frontend | React 19, React Router 8, Vite 8, Tailwind CSS v4, daisyUI, Axios, Lucide icons |
| Backend | Node.js, Express 4, Mongoose 7 |
| Database | MongoDB (MongoDB Atlas recommended) |
| Rate limiting | Upstash Redis + @upstash/ratelimit |
| Tooling | ESLint 10, nodemon |
thinkboard/
├── backend/
│ └── src/
│ ├── config/
│ │ ├── db.js # Mongoose connection bootstrap
│ │ └── upstash.js # Upstash Redis rate-limiter instance
│ ├── controllers/
│ │ └── notesController.js # Request handlers for note CRUD
│ ├── middleware/
│ │ └── rateLimiter.js # Express rate-limiting middleware
│ ├── models/
│ │ └── notes.js # Mongoose note schema
│ ├── routes/
│ │ └── notesRoutes.js # /api/notes router
│ └── server.js # App entry point
├── frontend/
│ ├── public/ # Static assets
│ └── src/
│ ├── components/ # Navbar, NoteCard, loading & empty states
│ ├── lib/ # Axios instance, date helpers
│ ├── pages/ # Home, Create, Note detail
│ ├── App.jsx # Route definitions
│ └── main.jsx # React entry point
└── package.json # Root build/start scripts
- Node.js 18 or later and npm
- A MongoDB connection string (local instance or MongoDB Atlas)
- An Upstash Redis database (REST URL and token) — required, as the rate limiter runs on every request
git clone https://github.com/Alphax978/thinkboardv3.git
cd thinkboardv3Create backend/.env using the template in Environment Variables. This file is git-ignored and must never be committed.
npm install --prefix backend
npm install --prefix frontendStart the API server (defaults to port 5001):
npm run dev --prefix backendIn a second terminal, start the Vite dev server (defaults to port 5173):
npm run dev --prefix frontendOpen http://localhost:5173. In development the frontend targets http://localhost:5001/api, and the backend enables CORS for the Vite origin only.
All backend configuration lives in backend/.env:
# MongoDB connection string
MONGO_URI=mongodb+srv://<user>:<password>@<cluster>/<database>
# Port for the Express server (optional, defaults to 5001)
PORT=5001
# Upstash Redis REST credentials for rate limiting
UPSTASH_REDIS_REST_URL=https://<your-database>.upstash.io
UPSTASH_REDIS_REST_TOKEN=<your-token>
# "development" or "production"
NODE_ENV=development| Variable | Required | Description |
|---|---|---|
MONGO_URI |
Yes | MongoDB connection string. The process exits on connection failure. |
PORT |
No | HTTP port for the API server. Defaults to 5001. |
UPSTASH_REDIS_REST_URL |
Yes | Upstash Redis REST endpoint used by the rate limiter. |
UPSTASH_REDIS_REST_TOKEN |
Yes | Upstash Redis REST token. Treat as a secret. |
NODE_ENV |
Yes | Controls CORS and static-file serving. Set to production when deploying. |
The frontend requires no environment variables; it resolves its API base URL from Vite's build mode.
Base path: /api/notes
| Method | Endpoint | Description | Request body |
|---|---|---|---|
GET |
/api/notes |
List all notes, newest first | — |
GET |
/api/notes/:id |
Fetch a single note by id | — |
POST |
/api/notes |
Create a note | { "title": string, "content": string } |
PUT |
/api/notes/:id |
Update a note | { "title": string, "content": string } |
DELETE |
/api/notes/:id |
Delete a note | — |
{
"_id": "6532f1c8a4d3b2001f8e4a91",
"title": "Meeting notes",
"content": "Ship the release on Friday.",
"createdAt": "2026-01-14T09:12:04.512Z",
"updatedAt": "2026-01-14T09:12:04.512Z"
}| Code | Meaning |
|---|---|
200 |
Request succeeded |
201 |
Note created |
404 |
Note not found |
429 |
Rate limit exceeded — { "message": "Too many requests, try again later" } |
500 |
Internal server error |
A sliding-window limiter allows 100 requests per 60 seconds and applies to every route. Exceeding it returns 429; the frontend surfaces this with a dedicated banner rather than a generic error.
From the repository root:
npm run build # installs backend + frontend deps, then builds the frontend
npm start # starts the Express serverWith NODE_ENV=production, the server serves frontend/dist as static files and falls back to index.html for client-side routes, so the API and the SPA are hosted on the same origin. The static path is resolved relative to the backend working directory — start the server with npm start from the repository root (or from backend/) so the path resolves correctly.
This project is a learning-oriented reference implementation. Review the following before exposing it publicly:
- No authentication or authorisation. Every note is globally readable and writable — anyone who can reach the API can read, edit, or delete any note. Add an auth layer and per-user ownership checks before hosting real data.
- Rate limiting is global, not per-client. The limiter uses a single fixed key, so all traffic shares one 100 req/min budget. A single client can exhaust the quota for everyone. Key the limiter on client IP or authenticated user identity to fix this.
- No server-side input validation. Length caps on title and content are enforced only in the browser and can be bypassed by calling the API directly. Add schema-level
maxlengthand request validation. - Dependencies need updating.
npm auditcurrently reports known advisories in both workspaces, including a critical prototype-pollution issue in Mongoose 7.0.3 and multiple high-severity advisories in the Express 4.18.2 dependency tree. Runnpm audit fixinbackend/andfrontend/and re-test. - Secrets stay out of version control.
.envis git-ignored at every level. RotateMONGO_URIandUPSTASH_REDIS_REST_TOKENimmediately if either is ever committed or shared. - Consider hardening headers. The server sets no security headers; adding
helmetand enforcing HTTPS is recommended for any public deployment.
- User accounts with per-user note ownership
- Per-IP / per-user rate limiting
- Server-side request validation
- Search and tagging
- Automated test suite and CI
Contributions are welcome.
- Fork the repository and create a feature branch:
git checkout -b feature/my-change - Make your changes and run the linter:
npm run lint --prefix frontend - Commit with a descriptive message and open a pull request against
main
Released under the ISC License.