How I Built a Zodiac Compatibility API with Node.js, Express and MongoDB
Source: Dev.to
From a Simple Idea to a Production‑Ready API
Ever wondered how dating apps or astrology platforms calculate zodiac compatibility behind the scenes? What looks like a simple “match score” on the frontend is often the result of carefully structured data, well‑designed algorithms, and scalable backend architecture.
I faced this exact challenge while building a love‑compatibility feature for a real production website. The goal was straightforward: given two zodiac signs, return a meaningful compatibility score along with contextual insights. The execution required thoughtful design choices around data modeling, API structure, caching, and performance.
In this guide you’ll see how I built a Zodiac Compatibility API using Node.js, Express, and MongoDB—from initial architecture decisions to deployment considerations. The API is actively used in production and handles thousands of daily requests powering real compatibility checks.
By the end you’ll understand how to:
- Design a clean REST API
- Implement a deterministic compatibility algorithm
- Structure the project for scalability
- Deploy and monitor the service in production
Tech Stack and Prerequisites
Core Technologies
- Node.js – Runtime environment
- Express.js – REST API framework
- MongoDB – Data storage for compatibility matrices and metadata
- Mongoose – ODM for MongoDB
- Redis (optional) – Caching layer
- External Astrology API (optional) – For daily horoscope enrichment
Prerequisites
You should be comfortable with:
- JavaScript (ES6+)
- RESTful API concepts
- Basic MongoDB queries
- Express middleware patterns
Project Architecture and Folder Structure
A clean structure is critical for long‑term maintainability. Below is the layout I used:
/zodiac-compatibility-api
│
├─ src
│ ├─ controllers # Request handling logic
│ ├─ models # Mongoose schemas
│ ├─ routes # Express routers
│ ├─ services # Business logic (e.g., compatibility engine)
│ ├─ utils # Helper functions (validation, caching)
│ └─ index.js # Application entry point
│
├─ config # Environment‑specific settings
├─ tests # Unit / integration tests
└─ Dockerfile, docker‑compose.yml, etc.
This separation allows each layer—routing, logic, data, and utilities—to evolve independently.
Defining the Core API Endpoints
The API was designed with simplicity and extensibility in mind.
| Method | Endpoint | Description |
|---|---|---|
GET | /compatibility/:sign1/:sign2 | Returns compatibility score and description for two signs |
POST | /calculate-match | Accepts a JSON payload with signs (and optional user data) and returns a match result |
GET | /daily-horoscope/:sign (optional) | Enriches the response with the day’s horoscope |
GET | /health | Simple health‑check for monitoring |
Each endpoint focuses on a single responsibility and returns predictable JSON responses.
Modeling Zodiac Compatibility in MongoDB
Instead of hard‑coding all compatibility logic, I stored the compatibility matrix in MongoDB. This enables future tuning without redeploying the application.
Schema example (simplified):
// models/Compatibility.js
const { Schema, model } = require('mongoose');
const CompatibilitySchema = new Schema({
signA: { type: String, required: true, uppercase: true },
signB: { type: String, required: true, uppercase: true },
score: { type: Number, required: true }, // 0‑100
shortDescription: { type: String },
longDescription: { type: String },
// optional: { shortTermScore, longTermScore, ... }
}, { timestamps: true });
CompatibilitySchema.index({ signA: 1, signB: 1 }, { unique: true });
module.exports = model('Compatibility', CompatibilitySchema);
The collection supports:
- Bidirectional lookup – queries work regardless of sign order.
- Rich descriptions – short and long text for UI display.
- Future extensions – e.g., separate scores for short‑term vs long‑term relationships.
Basic Scoring Function
The core algorithm is intentionally deterministic and easy to tune.
// services/compatibilityService.js
/**
* Calculate compatibility between two zodiac signs.
* @param {string} sign1 - First sign (case‑insensitive)
* @param {string} sign2 - Second sign (case‑insensitive)
* @returns {Promise} Compatibility document
*/
async function getCompatibility(sign1, sign2) {
const [a, b] = [sign1.toUpperCase(), sign2.toUpperCase()];
// Try direct order first, then reversed (bidirectional)
let result = await Compatibility.findOne({ signA: a, signB: b });
if (!result) {
result = await Compatibility.findOne({ signA: b, signB: a });
}
if (!result) {
throw new Error('Compatibility data not found for the given signs.');
}
return result;
}
Deterministic output → users see the same score every time.
Easy tuning → adjust score or descriptions directly in the DB.
Express Controller Implementation
The controller validates input, invokes the service, and formats the response.
// controllers/compatibilityController.js
const { getCompatibility } = require('../services/compatibilityService');
const { validateSign } = require('../utils/validation');
exports.getCompatibility = async (req, res) => {
try {
const { sign1, sign2 } = req.params;
// Input validation
if (!validateSign(sign1) || !validateSign(sign2)) {
return res.status(400).json({ error: 'Invalid zodiac sign supplied.' });
}
const data = await getCompatibility(sign1, sign2);
res.json({
signA: data.signA,
signB: data.signB,
score: data.score,
shortDescription: data.shortDescription,
longDescription: data.longDescription,
});
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Internal server error.' });
}
};
Performance Optimization with Caching
Compatibility results rarely change, making them ideal candidates for caching.
Redis Strategy
- Cache key:
compatibility:{signA}:{signB}(signs stored in uppercase) - TTL: 24 hours
- Pattern: Cache‑aside – check Redis first, fall back to MongoDB, then populate the cache.
// utils/cache.js
const redis = require('redis');
const client = redis.createClient({ url: process.env.REDIS_URL });
async function getCachedCompatibility(key) {
const cached = await client.get(key);
return cached ? JSON.parse(cached) : null;
}
async function setCachedCompatibility(key, value) {
await client.setEx(key, 86400, JSON.stringify(value)); // 24h TTL
}
In production this reduced database load by over 70 % during traffic spikes.
Error Handling and Validation
Astrology users may input invalid data, so defensive coding is essential.
- Whitelist validation – only the 12 (or 13, if including Ophiuchus) standard signs are accepted.
- Case normalization –
cancer,Cancer, andCANCERare treated identically. - Consistent HTTP status codes –
400for client errors,404when a pair isn’t found,500for server failures.
// utils/validation.js
const VALID_SIGNS = [
'ARIES','TAURUS','GEMINI','CANCER','LEO','VIRGO',
'LIBRA','SCORPIO','SAGITTARIUS','CAPRICORN','AQUARIUS','PISCES'
];
function validateSign(sign) {
return VALID_SIGNS.includes(sign.toUpperCase());
}
module.exports = { validateSign };
Deployment Strategy
The API runs in containers, making it portable across environments.
Deployment Stack
| Component | Service |
|---|---|
| Runtime | Node.js (LTS) |
| Database | MongoDB Atlas (managed) |
| Cache | Redis (managed or self‑hosted) |
| CI/CD | GitHub Actions – build, test, push Docker image |
| Orchestration | Docker Compose (local) / Kubernetes (production) |
| Config | Environment variables (dotenv) |
Zero‑downtime deployments are achieved by rolling updates behind a load balancer.
Lessons Learned
- Even “fun” features need solid engineering foundations.
- Deterministic logic builds user trust.
- Caching is essential at scale.
- Clean architecture saves time in the long run.
- Real‑world usage surfaces edge cases that tutorials rarely cover.
Where This API Goes Next
Planned improvements:
- Personalized birth‑chart analysis – combine sun, moon, and rising signs.
- Composite compatibility scoring – blend short‑term and long‑term metrics.
- Machine‑learning‑assisted tuning – use user feedback to adjust scores.
- Public API documentation – Swagger/OpenAPI spec for external developers.
Conclusion
Building a Zodiac Compatibility API with Node.js, Express, and MongoDB demonstrates how creativity can be paired with disciplined engineering. What started as a niche feature evolved into a scalable backend service that supports real users every day.
If you’re interested in the intersection of astrology, data modeling, and backend development, this project offers a practical blueprint you can adapt to your own ideas.