Skip to content

FULL-STACK / MARCH – APRIL 2026

ThinkBoard

A notes app with a rate-limited Express API.

Built on my own.

Source code
MY ROLE
Solo project
KEY RESULT
50 requests per 60 seconds, then HTTP 429
DECISION
Use a sliding-window limiter, then question the shared key when fairness broke down.
EVIDENCE
Interactive reproduction on this site plus the exact middleware source.
Notes dashboard
Create a note
Validation feedback
Successful save

How a request flows

  1. React + TailwindAxios client, toasts
  2. Rate limiterUpstash Redis, sliding window
  3. Express routes/api/notes CRUD
  4. MongoDBMongoose schema, timestamps

Can you lock out another visitor?

This is the same sliding-window rule ThinkBoard runs in Redis, scaled to 10 requests per 10 seconds. Start with the shared key, burst your requests, then ask another visitor to send one.

Rate-limit key
Key "my-rate-limit" (everyone)0 / 10 used

Waiting for the first request

  1. Responses appear here.

Why

I wanted a small app where I owned every layer, so I could follow a single request from a button click to the database and back.

What I built

  1. 01

    Built the React interface and Express API for full create, read, update, and delete on notes.

  2. 02

    Added Upstash Redis sliding-window rate limiting as Express middleware: 50 requests per 60 seconds, then HTTP 429.

  3. 03

    Handled 429 in the interface with a dedicated rate-limited screen instead of a generic error.

  4. 04

    Required title and content in both the form and the Mongoose schema, and added toast messages so every create, edit, and delete reports its result.

The code

backend/src/middleware/rateLimiter.js
// config/upstash.js: 50 requests per 60 seconds, sliding window
const rateLimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(50, "60 s"),
});

// middleware/rateLimiter.js
const ratelimiter = async (req, res, next) => {
  try {
    const { success } = await ratelimit.limit("my-rate-limit");
    if (!success) {
      return res.status(429).json({
        message: "Too many requests, please try again later",
      });
    }
    next();
  } catch (error) {
    next(error);
  }
};
Every request passes through this before reaching a route. The fixed key is the trade-off described below.

Outcome

A working notes app where every action gives clear feedback, and the API protects itself (and the Upstash free tier) from bursts of requests.

The trade-off

The limiter uses one fixed key, so all visitors share a single 50-request budget. That kept the code to a few lines and protects the free-tier quota, but one busy client can lock everyone out. Keying by IP address or user would isolate them, at the cost of more Redis keys.

What I learned

A rate limiter is only as fair as its key. Choosing what to count matters as much as the limit itself.

What I'd fix next

Key the limiter by IP, and return 400 with the field name when Mongoose validation fails. Right now a missing title falls through to a generic 500.

Go deeper

What a sliding-window rate limiter actually counts4 min read

Built with

  • React
  • JavaScript
  • Node.js
  • Express
  • MongoDB
  • Redis

Working on something like ThinkBoard?

Tell me what you're building and I'll reply within two business days.

Open to co-op, internship and software roles in Vancouver, on-site or hybrid. Graduating May 2027.