Fort Jesus Server: Architecting the API for a Heritage Museum Platform
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:
- Upload via
multerto a temporary directory - Validate format and minimum resolution (>800px)
- Upload to Cloudinary with automatic WebP conversion
- Store the Cloudinary
public_idin MongoDB (not the full URL) - 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.