Back to Blog
Backend Development

Fort Jesus Server: Architecting the API for a Heritage Museum Platform

View Repository on GitHub

Why a Dedicated Server?

When building the Fort Jesus digital platform, I made a deliberate choice to separate the client and server into distinct repositories. This decoupling gives the museum team flexibility to:

  • Swap the frontend independently of backend changes
  • Use the API for a potential mobile app in the future
  • Scale the server independently if traffic spikes around heritage events

Tech Stack

  • Node.js + Express — REST API server
  • MongoDB + Mongoose — Document store for exhibits and bookings
  • Cloudinary — Image storage and transformation pipeline
  • JWT — Authentication for the admin panel
  • Multer — File upload handling for artifact images

API Structure

/api/exhibits        GET, POST, PUT, DELETE
/api/artifacts       GET, POST, PUT, DELETE  
/api/tours           GET (public), POST (admin)
/api/bookings        POST (public), GET (admin)
/api/auth/login      POST
/api/auth/refresh    POST

Data Modeling

Museums have complex hierarchical content. I settled on this structure:

// Exhibit contains many Artifacts
const ExhibitSchema = new Schema({
  title: String,
  period: String,           // "Portuguese Era", "Omani Period"
  description: String,
  coverImage: String,       // Cloudinary public ID
  artifacts: [{ type: ObjectId, ref: 'Artifact' }],
  publishedAt: Date,
});

const ArtifactSchema = new Schema({
  name: String,
  origin: String,
  dateEstimated: String,    // "circa 1600s" — often approximate
  images: [String],         // Multiple Cloudinary IDs
  description: String,
  audioGuide: String,       // Optional URL for audio description
});

Image Pipeline

Artifact photography arrives in all shapes and sizes from the museum's cameras. The pipeline:

  1. Upload via multer to a temporary directory
  2. Validate format and minimum resolution (>800px)
  3. Upload to Cloudinary with automatic WebP conversion
  4. Store the Cloudinary public_id in MongoDB (not the full URL)
  5. Generate responsive URLs on-demand: cloudinary.url(publicId, { width: 800, crop: 'limit' })

Authentication

Admin routes are protected with JWT. The museum's curatorial team uses a simple admin panel (separate repo) to manage content. I implemented refresh token rotation — access tokens expire after 15 minutes, refresh tokens after 7 days.

Key Learning

Building APIs for non-technical clients taught me to think about data durability. Museum data is long-lived and precious. I implemented soft deletes (a deletedAt timestamp) rather than hard deletes — no artifact record is ever truly removed.

View on GitHub →