Skip to main content
Mukerem Shifa
Back to projects

Systems · 2025 — Present

Multi-Tenant Learning Management System

A role-based learning management system for managing courses, enrollments, progress, and academic workflows, with a React client and a Hono/TypeScript API backed by PostgreSQL and Drizzle ORM.

Role
Full-stack engineer, responsible for architecture, API design, and frontend integration
Timeline
2025 — Present
Team
Solo
Status
In progress
Stack
  • TypeScript
  • JavaScript
  • React
  • Vite
  • TanStack Query
  • Axios
  • Node.js
  • Hono
  • PostgreSQL
  • Drizzle ORM
  • Zod
  • AWS S3
The LMS instructor dashboard: tiles for open courses, active students, items to grade and median score above a learning-activity chart, a needs-attention queue and per-course submission progress, beside the headline “Every course, every learner, one system.”

Overview

BB LMS is built as a two-package workspace: a React 19 + Vite single-page app in `client/` and a Hono + TypeScript backend in `server/`. The frontend handles role-based user experiences and server-state synchronization, while the backend exposes modular domain APIs for authentication, users, courses, and related workflows.

The client communicates through a shared Axios layer with interceptors that attach authentication context and normalize error responses. Data access is organized through service modules and hooks, with TanStack Query used for caching, request lifecycle handling, and consistent async UX patterns across screens.

The backend applies a deliberate middleware and route-order strategy: public auth routes first, then global guards (`authGuard`, followed by `passGuard`) before protected modules. Business logic lives in service layers and surfaces expected failures via structured `AppError` objects so central error handling can return uniform API error contracts.

Key features

  • Layered modular backend architecture

    Domain modules are organized under `server/src/modules/<name>/` with route, schema, and service separation. This keeps transport concerns, validation, and business logic isolated, making features easier to evolve without cross-module coupling.

  • Guard-driven access flow

    Authentication and account-state checks are enforced globally in a strict order. `authGuard` verifies identity, and `passGuard` enforces account status and forced password-change policy before protected resources are accessed.

  • Consistent API error model

    Expected domain failures are raised as `AppError` and handled centrally in `app.onError`, alongside typed handling for `SyntaxError` and `ZodError`. This yields predictable JSON error responses across the platform.

  • Typed validation at route boundaries

    Request payloads are validated with Zod in route handlers before service execution. Input constraints are explicit and fail fast, reducing downstream runtime ambiguity.

  • Sqids-based public identifiers

    User-facing identifiers use Sqids rather than exposing raw integer database IDs, improving external URL hygiene and reducing accidental leakage of internal key patterns.

  • Frontend data access conventions

    Client-side server-state flows through TanStack Query hooks and service modules rather than ad-hoc component fetch logic, improving cache behavior, consistency, and testability of data interactions.

  • Presigned S3 upload workflow

    File upload is handled via presigned URLs so binary data does not pass through the API process. This reduces backend load and aligns storage access with cloud-native patterns.

  • Migration-first database workflow

    Schema changes are intended through Drizzle schema edits followed by generated SQL migrations and migration application, keeping database evolution explicit and reproducible.

Lessons learned

  • Middleware and route registration order is architecture, not plumbing; auth behavior depends on that sequence.
  • A central typed error contract (`AppError` + global handler) simplifies both frontend error UX and backend maintenance.
  • Strict module boundaries (route/schema/service) make refactors safer in growing codebases.
  • Using query libraries and shared HTTP clients early prevents drift into inconsistent data-fetching patterns.
  • Migration-driven schema changes are slower short-term but significantly safer than direct schema push habits.

Case study

Challenge
As protected domains expanded, there was risk of inconsistent authorization and error semantics across modules, especially when teams add routes quickly and validations drift.
Decision
Enforce a common flow: mount public auth routes first, apply global guards once, validate with Zod at route boundaries, and require service-layer business failures to throw `AppError` for centralized formatting.
Outcome
Protected endpoints now follow a predictable lifecycle from auth to validation to service execution, and clients receive stable error shapes. This improved maintainability and reduced integration friction between frontend hooks and backend APIs.

Tell me what you are building.

I read everything that arrives. If you are hiring, scoping a project, or stuck on a multi-tenancy or LLM integration problem, a few sentences about the constraint is enough to start a useful conversation.