FULL-STACK / SPRING 2026

SkillMatch

A workshop marketplace where students attend, review, and host short classes.

MY ROLE
Backend & DevOps lead, 3-person team
KEY RESULT
Top committer: 114 of ~245 commits
TEAM
BCIT team project with Clayton Liang and Ky Thai. I wrote 114 of roughly 245 commits in the team repo.

How a request flows

  1. React + ViteTypeScript SPA, lazy routes
  2. Express APIVercel service at /_/backend
  3. JWT middlewareprotectRoute on writes
  4. MongoDB AtlasMongoose models, keepalive ping

Why

Students wanted a simple way to find focused workshops outside their regular courses, and to run one themselves if they had a skill to share.

What I built

  1. 01

    Built the attend-and-review flow: completing checkout registers you as an attendee, which then unlocks a star-rating review form.

  2. 02

    Wrote the Express routes for attending, reviewing, and hosting a workshop, behind the JWT middleware that protects them.

  3. 03

    Built the host-a-workshop flow end to end: form, preview, confirmation, and a POST route that saves to MongoDB, with form state restored when you go back to edit.

  4. 04

    Added capacity feedback: the attendance card changes colour at 75% and 90% full, and the Attend button is disabled once a workshop is full.

  5. 05

    Cut first-load time by preloading key routes, splitting React and UI libraries into separately cached chunks, and pinging MongoDB Atlas every five minutes so the first request of the day is not a cold start.

  6. 06

    Replaced browser alert() errors with toast notifications and wrote a seed script for realistic workshop and user data.

The code

backend/routes/workshopRoutes.jsView on GitHub
router.post('/:id/attend', protectRoute, async (req, res) => {
  try {
    const workshop = await Workshop.findById(req.params.id);
    if (!workshop) return res.status(404).json({ error: 'Not found' });
    if (workshop.attendees.includes(req.user._id))
      return res.status(400).json({ error: 'Already attending' });

    workshop.attendees.push(req.user._id);
    await workshop.save();
    res.json(workshop);
  } catch (err) {
    res.status(500).json({ error: 'Failed to attend' });
  }
});
The attend route. It blocks duplicates but not overbooking, which is the trade-off described below.

Outcome

A live marketplace where students can browse, check out, attend, and review workshops, and anyone can host one. The frontend and Express API deploy together on Vercel.

The trade-off

Workshop capacity is checked in the interface, not enforced by the API. That was quick to build and gives instant feedback, but the attend route only checks that you are not already attending, so two people taking the last seat at the same moment would both get in. The API should be the source of truth for anything a user could race.

What I learned

Reading my own route back months later, the gaps are on the server, not the screen. A disabled button is a hint; the rule has to live in the API.

What I'd fix next

Make the attend route atomic with a single conditional update (only add the user if the attendee count is still below the seat count), and fix the owner check, which compares an ObjectId to a string and so never matches. Then add integration tests for both.

Go deeper

The race condition in SkillMatch’s attend route5 min read

Built with

  • React
  • TypeScript
  • Node.js
  • Express
  • MongoDB
  • JWT
  • Vercel