FULL-STACK / MARCH – APRIL 2026

ThinkBoard

A notes app with a rate-limited Express API.

MY ROLE
Solo project
KEY RESULT
50 requests per 60 seconds, then HTTP 429
TEAM
Built on my own.

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

Try the rate limiter

The same sliding-window rule ThinkBoard runs in Redis, simulated in your browser and scaled to 10 requests per 10 seconds. Send a burst, then see what happens to the other visitor.

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.jsView on GitHub
// 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