How to Build a Hinge-Style Dating App Algorithm with ExpressJS and TypeScript

2025-10-03

How to Build a Hinge-Style Dating App Algorithm with ExpressJS and TypeScript

Building a dating app like Hinge requires more than just swiping mechanics. Hinge's success lies in its sophisticated matching algorithm that prioritizes meaningful connections over superficial attraction. In this comprehensive guide, we'll build a Hinge-inspired dating app backend using ExpressJS and TypeScript, focusing on compatibility scoring and smart matching without machine learning.

Understanding Hinge's Algorithm Philosophy

Hinge differentiates itself by being "designed to be deleted." Their algorithm focuses on:

  1. Compatibility over Attraction: Matching based on shared interests, values, and lifestyle
  2. Engagement Quality: Prioritizing users who engage meaningfully with profiles
  3. Preference Learning: Adapting to user behavior and feedback
  4. Fresh Faces: Ensuring variety in recommendations
  5. Activity Recency: Favoring active users

MVP File Structure

Let's start by designing a clean, scalable file structure for our dating app backend:

dating-app-backend/
├── src/
│   ├── controllers/
│   │   ├── auth.controller.ts
│   │   ├── user.controller.ts
│   │   ├── match.controller.ts
│   │   └── recommendation.controller.ts
│   ├── middleware/
│   │   ├── auth.middleware.ts
│   │   ├── validation.middleware.ts
│   │   └── rateLimit.middleware.ts
│   ├── models/
│   │   ├── User.model.ts
│   │   ├── Profile.model.ts
│   │   ├── Match.model.ts
│   │   ├── Like.model.ts
│   │   └── Interaction.model.ts
│   ├── services/
│   │   ├── auth.service.ts
│   │   ├── matching.service.ts
│   │   ├── recommendation.service.ts
│   │   ├── compatibility.service.ts
│   │   └── notification.service.ts
│   ├── utils/
│   │   ├── database.ts
│   │   ├── logger.ts
│   │   ├── validators.ts
│   │   └── constants.ts
│   ├── routes/
│   │   ├── auth.routes.ts
│   │   ├── user.routes.ts
│   │   ├── match.routes.ts
│   │   └── recommendation.routes.ts
│   ├── types/
│   │   ├── user.types.ts
│   │   ├── match.types.ts
│   │   └── api.types.ts
│   └── app.ts
├── package.json
├── tsconfig.json
└── .env.example

Core Data Models

User Profile Model

// src/models/User.model.ts
export interface UserProfile {
  id: string;
  email: string;
  firstName: string;
  lastName: string;
  age: number;
  gender: 'male' | 'female' | 'non-binary' | 'other';
  interestedIn: ('male' | 'female' | 'non-binary' | 'other')[];
  location: {
    latitude: number;
    longitude: number;
    city: string;
    state: string;
  };
  photos: string[];
  prompts: {
    question: string;
    answer: string;
    photoUrl?: string;
  }[];
  preferences: {
    ageRange: { min: number; max: number };
    maxDistance: number;
    dealBreakers: string[];
  };
  lifestyle: {
    education: string;
    occupation: string;
    religion?: string;
    politicalViews?: string;
    smokingStatus: 'never' | 'sometimes' | 'regularly';
    drinkingStatus: 'never' | 'sometimes' | 'regularly';
    exerciseFrequency: 'never' | 'rarely' | 'sometimes' | 'regularly';
  };
  interests: string[];
  values: string[];
  isActive: boolean;
  lastActive: Date;
  createdAt: Date;
  updatedAt: Date;
}

Interaction Tracking Model

// src/models/Interaction.model.ts
export interface UserInteraction {
  id: string;
  userId: string;
  targetUserId: string;
  type: 'like' | 'pass' | 'comment' | 'view';
  promptIndex?: number; // Which prompt they interacted with
  comment?: string;
  timestamp: Date;
}

export interface MatchQuality {
  userId: string;
  targetUserId: string;
  compatibilityScore: number;
  factors: {
    sharedInterests: number;
    lifestyleCompatibility: number;
    valueAlignment: number;
    proximityScore: number;
    ageCompatibility: number;
    activityScore: number;
  };
  calculatedAt: Date;
}

The Compatibility Algorithm

The heart of our Hinge-inspired algorithm is the compatibility scoring system. Here's how we calculate meaningful matches:

Compatibility Service

// src/services/compatibility.service.ts
import { UserProfile, MatchQuality } from '../models';

export class CompatibilityService {
  
  /**
   * Calculate comprehensive compatibility score between two users
   */
  static calculateCompatibility(user1: UserProfile, user2: UserProfile): MatchQuality {
    const factors = {
      sharedInterests: this.calculateSharedInterests(user1, user2),
      lifestyleCompatibility: this.calculateLifestyleCompatibility(user1, user2),
      valueAlignment: this.calculateValueAlignment(user1, user2),
      proximityScore: this.calculateProximityScore(user1, user2),
      ageCompatibility: this.calculateAgeCompatibility(user1, user2),
      activityScore: this.calculateActivityScore(user2)
    };

    // Weighted compatibility score
    const compatibilityScore = (
      factors.sharedInterests * 0.25 +
      factors.lifestyleCompatibility * 0.20 +
      factors.valueAlignment * 0.20 +
      factors.proximityScore * 0.15 +
      factors.ageCompatibility * 0.10 +
      factors.activityScore * 0.10
    );

    return {
      userId: user1.id,
      targetUserId: user2.id,
      compatibilityScore: Math.round(compatibilityScore * 100) / 100,
      factors,
      calculatedAt: new Date()
    };
  }

  /**
   * Calculate shared interests score (0-100)
   */
  private static calculateSharedInterests(user1: UserProfile, user2: UserProfile): number {
    const interests1 = new Set(user1.interests);
    const interests2 = new Set(user2.interests);
    
    const intersection = new Set([...interests1].filter(x => interests2.has(x)));
    const union = new Set([...interests1, ...interests2]);
    
    if (union.size === 0) return 0;
    
    // Jaccard similarity with bonus for multiple shared interests
    const jaccardSimilarity = intersection.size / union.size;
    const sharedCount = intersection.size;
    const bonus = Math.min(sharedCount * 0.1, 0.3); // Max 30% bonus
    
    return Math.min((jaccardSimilarity + bonus) * 100, 100);
  }

  /**
   * Calculate lifestyle compatibility (0-100)
   */
  private static calculateLifestyleCompatibility(user1: UserProfile, user2: UserProfile): number {
    let score = 0;
    let factors = 0;

    // Education compatibility
    if (user1.lifestyle.education && user2.lifestyle.education) {
      const educationLevels = ['high-school', 'some-college', 'bachelors', 'masters', 'phd'];
      const level1 = educationLevels.indexOf(user1.lifestyle.education);
      const level2 = educationLevels.indexOf(user2.lifestyle.education);
      
      if (level1 !== -1 && level2 !== -1) {
        const diff = Math.abs(level1 - level2);
        score += Math.max(0, 100 - (diff * 20)); // Penalize large education gaps
        factors++;
      }
    }

    // Lifestyle habits compatibility
    const habits = ['smokingStatus', 'drinkingStatus', 'exerciseFrequency'] as const;
    
    habits.forEach(habit => {
      const value1 = user1.lifestyle[habit];
      const value2 = user2.lifestyle[habit];
      
      if (value1 && value2) {
        if (value1 === value2) {
          score += 100;
        } else {
          // Partial compatibility for similar habits
          const compatibility = this.getHabitCompatibility(habit, value1, value2);
          score += compatibility;
        }
        factors++;
      }
    });

    return factors > 0 ? score / factors : 50; // Default neutral score
  }

  /**
   * Calculate value alignment score (0-100)
   */
  private static calculateValueAlignment(user1: UserProfile, user2: UserProfile): number {
    const values1 = new Set(user1.values);
    const values2 = new Set(user2.values);
    
    if (values1.size === 0 || values2.size === 0) return 50; // Neutral if no values specified
    
    const intersection = new Set([...values1].filter(x => values2.has(x)));
    const union = new Set([...values1, ...values2]);
    
    // Strong emphasis on shared core values
    const sharedValueScore = (intersection.size / Math.min(values1.size, values2.size)) * 100;
    
    // Check for conflicting values (deal breakers)
    const conflictPenalty = this.calculateValueConflicts(user1, user2);
    
    return Math.max(0, sharedValueScore - conflictPenalty);
  }

  /**
   * Calculate proximity score based on distance (0-100)
   */
  private static calculateProximityScore(user1: UserProfile, user2: UserProfile): number {
    const distance = this.calculateDistance(
      user1.location.latitude, user1.location.longitude,
      user2.location.latitude, user2.location.longitude
    );

    const maxDistance = user1.preferences.maxDistance;
    
    if (distance > maxDistance) return 0;
    
    // Score decreases with distance, but not linearly
    const normalizedDistance = distance / maxDistance;
    return Math.max(0, 100 * (1 - Math.pow(normalizedDistance, 0.5)));
  }

  /**
   * Calculate age compatibility (0-100)
   */
  private static calculateAgeCompatibility(user1: UserProfile, user2: UserProfile): number {
    const { min, max } = user1.preferences.ageRange;
    
    if (user2.age < min || user2.age > max) return 0;
    
    // Prefer ages closer to user's age
    const ageDiff = Math.abs(user1.age - user2.age);
    const maxAgeDiff = Math.max(user1.age - min, max - user1.age);
    
    return Math.max(0, 100 * (1 - (ageDiff / maxAgeDiff)));
  }

  /**
   * Calculate activity score (0-100)
   */
  private static calculateActivityScore(user: UserProfile): number {
    const now = new Date();
    const lastActive = new Date(user.lastActive);
    const hoursSinceActive = (now.getTime() - lastActive.getTime()) / (1000 * 60 * 60);
    
    if (!user.isActive) return 0;
    
    // Score decreases with time since last activity
    if (hoursSinceActive <= 24) return 100;
    if (hoursSinceActive <= 72) return 80;
    if (hoursSinceActive <= 168) return 60; // 1 week
    if (hoursSinceActive <= 720) return 40; // 1 month
    
    return 20; // Minimum score for active users
  }

  // Helper methods
  private static getHabitCompatibility(
    habit: 'smokingStatus' | 'drinkingStatus' | 'exerciseFrequency',
    value1: string,
    value2: string
  ): number {
    const compatibilityMatrix: Record<string, Record<string, number>> = {
      smokingStatus: {
        'never-sometimes': 30,
        'never-regularly': 0,
        'sometimes-regularly': 60
      },
      drinkingStatus: {
        'never-sometimes': 50,
        'never-regularly': 20,
        'sometimes-regularly': 70
      },
      exerciseFrequency: {
        'never-rarely': 70,
        'never-sometimes': 40,
        'never-regularly': 20,
        'rarely-sometimes': 80,
        'rarely-regularly': 50,
        'sometimes-regularly': 80
      }
    };

    const key = `${value1}-${value2}`;
    const reverseKey = `${value2}-${value1}`;
    
    return compatibilityMatrix[habit]?.[key] || 
           compatibilityMatrix[habit]?.[reverseKey] || 
           30; // Default partial compatibility
  }

  private static calculateValueConflicts(user1: UserProfile, user2: UserProfile): number {
    // Define conflicting value pairs
    const conflicts = [
      ['religious', 'atheist'],
      ['conservative', 'liberal'],
      ['family-oriented', 'career-focused'],
      // Add more as needed
    ];

    let conflictPenalty = 0;
    
    conflicts.forEach(([value1, value2]) => {
      if ((user1.values.includes(value1) && user2.values.includes(value2)) ||
          (user1.values.includes(value2) && user2.values.includes(value1))) {
        conflictPenalty += 30; // Heavy penalty for conflicting core values
      }
    });

    return conflictPenalty;
  }

  private static calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
    const R = 3959; // Earth's radius in miles
    const dLat = this.toRadians(lat2 - lat1);
    const dLon = this.toRadians(lon2 - lon1);
    
    const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
              Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) *
              Math.sin(dLon / 2) * Math.sin(dLon / 2);
    
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return R * c;
  }

  private static toRadians(degrees: number): number {
    return degrees * (Math.PI / 180);
  }
}

Recommendation Engine

Now let's build the recommendation service that uses our compatibility algorithm:

// src/services/recommendation.service.ts
import { UserProfile, UserInteraction, MatchQuality } from '../models';
import { CompatibilityService } from './compatibility.service';

export class RecommendationService {
  
  /**
   * Get personalized recommendations for a user
   */
  static async getRecommendations(
    userId: string,
    limit: number = 10
  ): Promise<UserProfile[]> {
    
    const user = await this.getUserProfile(userId);
    const interactions = await this.getUserInteractions(userId);
    const potentialMatches = await this.getPotentialMatches(user, interactions);
    
    // Calculate compatibility scores
    const scoredMatches = potentialMatches.map(match => ({
      profile: match,
      compatibility: CompatibilityService.calculateCompatibility(user, match)
    }));

    // Apply Hinge-style ranking algorithm
    const rankedMatches = this.applyHingeRanking(scoredMatches, interactions, user);
    
    return rankedMatches
      .slice(0, limit)
      .map(match => match.profile);
  }

  /**
   * Apply Hinge-inspired ranking algorithm
   */
  private static applyHingeRanking(
    scoredMatches: Array<{ profile: UserProfile; compatibility: MatchQuality }>,
    interactions: UserInteraction[],
    user: UserProfile
  ) {
    
    return scoredMatches
      .map(match => {
        let finalScore = match.compatibility.compatibilityScore;
        
        // Boost score for users with high engagement potential
        finalScore += this.calculateEngagementBoost(match.profile, user);
        
        // Apply freshness factor (prioritize users not seen recently)
        finalScore += this.calculateFreshnessBoost(match.profile.id, interactions);
        
        // Apply diversity factor (ensure variety in recommendations)
        finalScore += this.calculateDiversityBoost(match.profile, scoredMatches);
        
        // Penalize users who haven't been active recently
        finalScore *= this.getActivityMultiplier(match.profile);
        
        return {
          ...match,
          finalScore
        };
      })
      .sort((a, b) => b.finalScore - a.finalScore);
  }

  /**
   * Calculate engagement boost based on profile completeness and prompt quality
   */
  private static calculateEngagementBoost(profile: UserProfile, user: UserProfile): number {
    let boost = 0;
    
    // Boost for complete profiles
    const completenessScore = this.calculateProfileCompleteness(profile);
    boost += completenessScore * 0.1;
    
    // Boost for interesting prompts that might spark conversation
    const promptQuality = this.calculatePromptQuality(profile.prompts);
    boost += promptQuality * 0.15;
    
    // Boost for mutual interests that could lead to good conversations
    const conversationPotential = this.calculateConversationPotential(profile, user);
    boost += conversationPotential * 0.1;
    
    return Math.min(boost, 20); // Cap boost at 20 points
  }

  /**
   * Calculate freshness boost to ensure variety
   */
  private static calculateFreshnessBoost(profileId: string, interactions: UserInteraction[]): number {
    const recentInteractions = interactions.filter(
      interaction => interaction.targetUserId === profileId &&
      Date.now() - interaction.timestamp.getTime() < 7 * 24 * 60 * 60 * 1000 // 7 days
    );
    
    if (recentInteractions.length === 0) return 10; // Fresh face bonus
    if (recentInteractions.length === 1) return 5;  // Seen once recently
    return -5; // Seen multiple times recently
  }

  /**
   * Calculate diversity boost to ensure varied recommendations
   */
  private static calculateDiversityBoost(
    profile: UserProfile,
    allMatches: Array<{ profile: UserProfile; compatibility: MatchQuality }>
  ): number {
    // Implement diversity logic based on occupation, interests, location, etc.
    // This is a simplified version
    const topMatches = allMatches.slice(0, 5);
    const occupations = topMatches.map(m => m.profile.lifestyle.occupation);
    const locations = topMatches.map(m => m.profile.location.city);
    
    let diversityBoost = 0;
    
    // Boost for different occupation
    if (!occupations.includes(profile.lifestyle.occupation)) {
      diversityBoost += 3;
    }
    
    // Boost for different location (but not too far)
    if (!locations.includes(profile.location.city)) {
      diversityBoost += 2;
    }
    
    return diversityBoost;
  }

  /**
   * Get activity multiplier based on recent activity
   */
  private static getActivityMultiplier(profile: UserProfile): number {
    const hoursSinceActive = (Date.now() - profile.lastActive.getTime()) / (1000 * 60 * 60);
    
    if (hoursSinceActive <= 24) return 1.0;   // Very active
    if (hoursSinceActive <= 72) return 0.95;  // Active
    if (hoursSinceActive <= 168) return 0.9;  // Moderately active
    if (hoursSinceActive <= 720) return 0.8;  // Less active
    
    return 0.7; // Inactive
  }

  // Helper methods
  private static calculateProfileCompleteness(profile: UserProfile): number {
    let completeness = 0;
    const totalFields = 10;
    
    if (profile.photos.length >= 3) completeness++;
    if (profile.prompts.length >= 3) completeness++;
    if (profile.lifestyle.education) completeness++;
    if (profile.lifestyle.occupation) completeness++;
    if (profile.interests.length >= 5) completeness++;
    if (profile.values.length >= 3) completeness++;
    if (profile.lifestyle.religion) completeness++;
    if (profile.lifestyle.politicalViews) completeness++;
    if (profile.prompts.some(p => p.photoUrl)) completeness++;
    if (profile.prompts.every(p => p.answer.length > 20)) completeness++;
    
    return (completeness / totalFields) * 100;
  }

  private static calculatePromptQuality(prompts: UserProfile['prompts']): number {
    if (prompts.length === 0) return 0;
    
    let qualityScore = 0;
    
    prompts.forEach(prompt => {
      // Longer, more thoughtful answers score higher
      if (prompt.answer.length > 50) qualityScore += 20;
      else if (prompt.answer.length > 20) qualityScore += 10;
      
      // Prompts with photos are more engaging
      if (prompt.photoUrl) qualityScore += 15;
      
      // Check for conversation starters (questions, interesting statements)
      if (prompt.answer.includes('?') || 
          prompt.answer.toLowerCase().includes('ask me') ||
          prompt.answer.toLowerCase().includes('tell me')) {
        qualityScore += 10;
      }
    });
    
    return Math.min(qualityScore / prompts.length, 100);
  }

  private static calculateConversationPotential(profile1: UserProfile, profile2: UserProfile): number {
    // Look for shared interests that could spark conversations
    const sharedInterests = profile1.interests.filter(interest => 
      profile2.interests.includes(interest)
    );
    
    // Certain interests are better conversation starters
    const conversationStarters = [
      'travel', 'cooking', 'music', 'movies', 'books', 'hiking', 
      'photography', 'art', 'sports', 'gaming'
    ];
    
    const goodConversationTopics = sharedInterests.filter(interest =>
      conversationStarters.includes(interest.toLowerCase())
    );
    
    return Math.min(goodConversationTopics.length * 15, 60);
  }

  // Placeholder methods - implement based on your database
  private static async getUserProfile(userId: string): Promise<UserProfile> {
    // Implement database query
    throw new Error('Not implemented');
  }

  private static async getUserInteractions(userId: string): Promise<UserInteraction[]> {
    // Implement database query
    throw new Error('Not implemented');
  }

  private static async getPotentialMatches(
    user: UserProfile, 
    interactions: UserInteraction[]
  ): Promise<UserProfile[]> {
    // Implement database query with filters:
    // - Exclude already interacted users
    // - Apply basic filters (age, distance, gender preferences)
    // - Limit to reasonable number for processing
    throw new Error('Not implemented');
  }
}

API Controllers

Let's create the Express controllers to expose our matching functionality:

// src/controllers/recommendation.controller.ts
import { Request, Response } from 'express';
import { RecommendationService } from '../services/recommendation.service';
import { CompatibilityService } from '../services/compatibility.service';

export class RecommendationController {
  
  /**
   * Get personalized recommendations for the authenticated user
   */
  static async getRecommendations(req: Request, res: Response) {
    try {
      const userId = req.user?.id; // Assuming auth middleware sets req.user
      const limit = parseInt(req.query.limit as string) || 10;
      
      if (!userId) {
        return res.status(401).json({ error: 'Authentication required' });
      }

      const recommendations = await RecommendationService.getRecommendations(userId, limit);
      
      res.json({
        success: true,
        data: recommendations,
        count: recommendations.length
      });
      
    } catch (error) {
      console.error('Error getting recommendations:', error);
      res.status(500).json({ error: 'Internal server error' });
    }
  }

  /**
   * Get detailed compatibility analysis between two users
   */
  static async getCompatibilityAnalysis(req: Request, res: Response) {
    try {
      const userId = req.user?.id;
      const { targetUserId } = req.params;
      
      if (!userId) {
        return res.status(401).json({ error: 'Authentication required' });
      }

      // Get user profiles (implement these methods)
      const user = await getUserProfile(userId);
      const targetUser = await getUserProfile(targetUserId);
      
      if (!user || !targetUser) {
        return res.status(404).json({ error: 'User not found' });
      }

      const compatibility = CompatibilityService.calculateCompatibility(user, targetUser);
      
      res.json({
        success: true,
        data: compatibility
      });
      
    } catch (error) {
      console.error('Error calculating compatibility:', error);
      res.status(500).json({ error: 'Internal server error' });
    }
  }

  /**
   * Record user interaction (like, pass, comment)
   */
  static async recordInteraction(req: Request, res: Response) {
    try {
      const userId = req.user?.id;
      const { targetUserId, type, promptIndex, comment } = req.body;
      
      if (!userId) {
        return res.status(401).json({ error: 'Authentication required' });
      }

      // Validate interaction type
      const validTypes = ['like', 'pass', 'comment', 'view'];
      if (!validTypes.includes(type)) {
        return res.status(400).json({ error: 'Invalid interaction type' });
      }

      // Record interaction (implement this method)
      const interaction = await recordUserInteraction({
        userId,
        targetUserId,
        type,
        promptIndex,
        comment,
        timestamp: new Date()
      });

      // Check if it's a mutual like (match)
      let isMatch = false;
      if (type === 'like') {
        isMatch = await checkForMutualLike(userId, targetUserId);
        
        if (isMatch) {
          await createMatch(userId, targetUserId);
          // Send notifications, etc.
        }
      }

      res.json({
        success: true,
        data: {
          interaction,
          isMatch
        }
      });
      
    } catch (error) {
      console.error('Error recording interaction:', error);
      res.status(500).json({ error: 'Internal server error' });
    }
  }
}

// Placeholder functions - implement based on your database
async function getUserProfile(userId: string) {
  // Implement database query
  throw new Error('Not implemented');
}

async function recordUserInteraction(interaction: any) {
  // Implement database insertion
  throw new Error('Not implemented');
}

async function checkForMutualLike(userId: string, targetUserId: string): Promise<boolean> {
  // Check if targetUser has liked userId
  throw new Error('Not implemented');
}

async function createMatch(userId: string, targetUserId: string) {
  // Create match record in database
  throw new Error('Not implemented');
}

Express Routes Setup

// src/routes/recommendation.routes.ts
import { Router } from 'express';
import { RecommendationController } from '../controllers/recommendation.controller';
import { authMiddleware } from '../middleware/auth.middleware';
import { rateLimitMiddleware } from '../middleware/rateLimit.middleware';

const router = Router();

// Apply authentication middleware to all routes
router.use(authMiddleware);

// Get personalized recommendations
router.get(
  '/',
  rateLimitMiddleware({ windowMs: 60000, max: 10 }), // 10 requests per minute
  RecommendationController.getRecommendations
);

// Get compatibility analysis
router.get(
  '/compatibility/:targetUserId',
  rateLimitMiddleware({ windowMs: 60000, max: 20 }),
  RecommendationController.getCompatibilityAnalysis
);

// Record user interaction
router.post(
  '/interact',
  rateLimitMiddleware({ windowMs: 60000, max: 30 }),
  RecommendationController.recordInteraction
);

export default router;

Main Application Setup

// src/app.ts
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import rateLimit from 'express-rate-limit';

import authRoutes from './routes/auth.routes';
import userRoutes from './routes/user.routes';
import recommendationRoutes from './routes/recommendation.routes';
import matchRoutes from './routes/match.routes';

const app = express();

// Security middleware
app.use(helmet());
app.use(cors({
  origin: process.env.FRONTEND_URL || 'http://localhost:3000',
  credentials: true
}));

// Rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});
app.use(limiter);

// Logging
app.use(morgan('combined'));

// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// Routes
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
app.use('/api/recommendations', recommendationRoutes);
app.use('/api/matches', matchRoutes);

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'OK', timestamp: new Date().toISOString() });
});

// Error handling middleware
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Something went wrong!' });
});

const PORT = process.env.PORT || 3001;

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

export default app;

Package.json Configuration

{
  "name": "hinge-style-dating-app",
  "version": "1.0.0",
  "description": "A Hinge-inspired dating app backend with sophisticated matching algorithm",
  "main": "dist/app.js",
  "scripts": {
    "start": "node dist/app.js",
    "dev": "nodemon src/app.ts",
    "build": "tsc",
    "test": "jest",
    "lint": "eslint src/**/*.ts",
    "lint:fix": "eslint src/**/*.ts --fix"
  },
  "dependencies": {
    "express": "^4.18.2",
    "cors": "^2.8.5",
    "helmet": "^7.0.0",
    "morgan": "^1.10.0",
    "express-rate-limit": "^6.8.1",
    "bcryptjs": "^2.4.3",
    "jsonwebtoken": "^9.0.1",
    "mongoose": "^7.4.0",
    "joi": "^17.9.2",
    "dotenv": "^16.3.1",
    "winston": "^3.10.0"
  },
  "devDependencies": {
    "@types/express": "^4.17.17",
    "@types/cors": "^2.8.13",
    "@types/morgan": "^1.9.4",
    "@types/bcryptjs": "^2.4.2",
    "@types/jsonwebtoken": "^9.0.2",
    "@types/node": "^20.4.2",
    "typescript": "^5.1.6",
    "nodemon": "^3.0.1",
    "ts-node": "^10.9.1",
    "@typescript-eslint/eslint-plugin": "^6.1.0",
    "@typescript-eslint/parser": "^6.1.0",
    "eslint": "^8.45.0",
    "jest": "^29.6.1",
    "@types/jest": "^29.5.3"
  }
}

Key Algorithm Features

1. Multi-Factor Compatibility Scoring

  • Shared Interests: Uses Jaccard similarity with bonuses for multiple matches
  • Lifestyle Compatibility: Considers education, habits, and life choices
  • Value Alignment: Emphasizes core values while penalizing conflicts
  • Proximity: Distance-based scoring with user preference consideration
  • Age Compatibility: Preference-based with proximity bonuses

2. Hinge-Inspired Ranking

  • Engagement Boost: Rewards complete profiles and conversation starters
  • Freshness Factor: Ensures variety by boosting unseen profiles
  • Diversity Scoring: Prevents echo chambers by promoting variety
  • Activity Weighting: Prioritizes recently active users

3. Learning and Adaptation

  • Interaction Tracking: Records all user behaviors for future optimization
  • Feedback Integration: Uses likes/passes to refine future recommendations
  • Temporal Patterns: Considers timing and frequency of interactions

Best Practices Implemented

  1. Type Safety: Full TypeScript implementation with strict typing
  2. Scalability: Modular architecture supporting horizontal scaling
  3. Security: Rate limiting, input validation, and secure authentication
  4. Performance: Efficient algorithms with caching opportunities
  5. Maintainability: Clean separation of concerns and comprehensive documentation

Conclusion

This Hinge-inspired dating app algorithm demonstrates how to create meaningful connections without machine learning. By focusing on compatibility factors that matter for long-term relationships and implementing smart ranking algorithms, we can build a dating app that truly helps users find compatible partners.

The key to success lies in the multi-dimensional compatibility scoring, thoughtful user experience design, and continuous refinement based on user behavior. This foundation provides an excellent starting point for building a sophisticated dating platform that prioritizes quality matches over quantity.

Remember to continuously test and refine your algorithm based on user feedback and success metrics. The best matching algorithms evolve with their user base and learn from real-world relationship outcomes.

← Back to Home