How to create server endpoints

  • Use the server to handle operations that cannot be performed in the browser due to security restrictions
  • All endpoints should be explicitly typed with TypeScript interfaces for request/response
  • Provide proper error handling with appropriate HTTP status codes
  • Use environment variables for sensitive configuration (API keys, database URLs, etc.)
  • Always validate and sanitize input data before processing

Server Architecture

  • CRITICAL: You MUST use ExpressWithRouteTracking class instead of standard Express for automatic route discovery
  • WARNING: If you use standard Express instead of ExpressWithRouteTracking, the API proxy functionality will break and your endpoints will not work properly
  • WARNING: ALL SERVERS MUST INCLUDE A /ping ENDPOINT FOR HEALTH CHECKS, OTHERWISE THEY WILL FAIL, this should be there by default, so DO NOT DELETE
  • Place all endpoint handlers in the src/index.ts file for simplicity
  • Use middleware for common functionality like authentication, logging, and validation
  • Implement proper CORS handling for cross-origin requests

Route Discovery for Hosted Platform

Why ExpressWithRouteTracking is Required

The ExpressWithRouteTracking class is essential for the hosted platform to automatically discover and configure your API endpoints. When your application is deployed:

  • The platform scans your server code to identify all registered routes
  • It automatically generates nginx configuration to proxy requests to your endpoints
  • Without route tracking, the platform cannot detect your API endpoints and they will not be accessible

This is why you must use ExpressWithRouteTracking instead of standard Express - it provides the route metadata that the hosting infrastructure requires to properly route requests to your application.

Security Best Practices

  • Never expose sensitive information in response bodies or logs
  • Use environment variables for all API keys, database credentials, and secrets
  • Validate all input parameters and sanitize data to prevent injection attacks
  • Implement rate limiting for public endpoints to prevent abuse
  • Use HTTPS in production and validate SSL certificates

CORS and External Services

  • Enable CORS for browser-based requests from your frontend
  • Use the server as a proxy to external APIs to hide API keys from the client
  • Implement proper error handling for external service failures
  • Cache external API responses when appropriate to improve performance
  • Use environment variables to configure external service endpoints

File System and OS Operations

  • Use Node.js built-in modules (fs, path, os) for file system operations
  • Implement proper error handling for file operations (permissions, not found, etc.)
  • Use absolute paths with proper path resolution for file operations
  • Implement file upload/download endpoints with size limits and type validation
  • Use streaming for large file operations to prevent memory issues

Database Operations

  • Use environment variables for database connection strings
  • Implement connection pooling for database operations
  • Use parameterized queries to prevent SQL injection
  • Implement proper transaction handling for multi-step operations
  • Add database connection health checks and error recovery

Error Handling

  • Use consistent error response format across all endpoints
  • Log errors with appropriate detail level (debug, info, warn, error)
  • Return appropriate HTTP status codes (400, 401, 403, 404, 500, etc.)
  • Provide meaningful error messages for client consumption
  • Implement global error handling middleware

Code Style

  • Use explicit TypeScript types for all request/response objects
  • Add JSDoc comments for endpoint descriptions and parameters
  • Include example usage in comments for complex endpoints
  • Use descriptive variable names that indicate purpose and type
  • Implement proper logging with structured data

Examples

External API Proxy with Secret Management

/**
 * Weather API proxy endpoint
 * 
 * Proxies requests to external weather service while hiding API key.
 * Handles rate limiting, caching, and error transformation.
 * 
 * @example
 * GET /api/weather?city=London&country=UK
 * Response: { temperature: 15, condition: "cloudy", humidity: 75 }
 */

import { Request, Response } from 'express';

interface WeatherRequest {
  city: string;
  country?: string;
  units?: 'metric' | 'imperial';
}

interface WeatherResponse {
  temperature: number;
  condition: string;
  humidity: number;
  timestamp: string;
}

interface ExternalWeatherResponse {
  main: {
    temp: number;
    humidity: number;
  };
  weather: Array<{
    main: string;
    description: string;
  }>;
}

// Validate and sanitize input parameters
const validateWeatherRequest = (query: any): WeatherRequest => {
  const { city, country, units } = query;
  
  if (!city || typeof city !== 'string' || city.trim().length === 0) {
    throw new Error('City parameter is required and must be a non-empty string');
  }
  
  if (country && typeof country !== 'string') {
    throw new Error('Country parameter must be a string');
  }
  
  if (units && !['metric', 'imperial'].includes(units)) {
    throw new Error('Units parameter must be either "metric" or "imperial"');
  }
  
  return {
    city: city.trim(),
    country: country?.trim(),
    units: units || 'metric'
  };
};

// Transform external API response to internal format
const transformWeatherResponse = (externalData: ExternalWeatherResponse): WeatherResponse => {
  return {
    temperature: Math.round(externalData.main.temp),
    condition: externalData.weather[0]?.main.toLowerCase() || 'unknown',
    humidity: externalData.main.humidity,
    timestamp: new Date().toISOString()
  };
};

// Weather endpoint implementation
app.get('/api/weather', async (req: Request, res: Response) => {
  try {
    // Validate and sanitize input
    const weatherRequest = validateWeatherRequest(req.query);
    
    // Get API key from environment (hidden from client)
    const apiKey = process.env.WEATHER_API_KEY;
    if (!apiKey) {
      console.error('Weather API key not configured');
      return res.status(500).json({ 
        error: 'Weather service temporarily unavailable' 
      });
    }
    
    // Build external API URL with secret key
    const baseUrl = 'https://api.openweathermap.org/data/2.5/weather';
    const params = new URLSearchParams({
      q: weatherRequest.country 
        ? `${weatherRequest.city},${weatherRequest.country}` 
        : weatherRequest.city,
      units: weatherRequest.units,
      appid: apiKey // API key hidden from client
    });
    
    // Make external API request
    const response = await fetch(`${baseUrl}?${params}`);
    
    if (!response.ok) {
      // Handle external API errors
      if (response.status === 401) {
        console.error('Invalid weather API key');
        return res.status(500).json({ 
          error: 'Weather service configuration error' 
        });
      }
      
      if (response.status === 404) {
        return res.status(404).json({ 
          error: 'City not found' 
        });
      }
      
      throw new Error(`Weather API error: ${response.status}`);
    }
    
    const externalData: ExternalWeatherResponse = await response.json();
    
    // Transform and return response
    const weatherData = transformWeatherResponse(externalData);
    
    res.json(weatherData);
    
  } catch (error) {
    console.error('Weather endpoint error:', error);
    
    if (error instanceof Error && error.message.includes('City parameter')) {
      return res.status(400).json({ error: error.message });
    }
    
    res.status(500).json({ 
      error: 'Failed to fetch weather data' 
    });
  }
});

File System Operations

/**
 * File upload endpoint
 * 
 * Handles file uploads with size limits, type validation, and secure storage.
 * Uses environment variables for configuration and hides file system paths.
 * 
 * @example
 * POST /api/upload
 * Content-Type: multipart/form-data
 * Body: file (image/jpeg, max 5MB)
 */

import multer from 'multer';
import { promises as fs } from 'fs';
import path from 'path';
import crypto from 'crypto';

interface UploadResponse {
  fileId: string;
  filename: string;
  size: number;
  mimeType: string;
  uploadedAt: string;
}

// Configure multer for file uploads
const upload = multer({
  storage: multer.memoryStorage(),
  limits: {
    fileSize: parseInt(process.env.MAX_FILE_SIZE || '5242880'), // 5MB default
    files: 1
  },
  fileFilter: (req, file, cb) => {
    // Validate file types
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
    if (!allowedTypes.includes(file.mimetype)) {
      return cb(new Error('Invalid file type. Only JPEG, PNG, and GIF are allowed.'));
    }
    cb(null, true);
  }
});

// Generate secure filename to prevent path traversal
const generateSecureFilename = (originalName: string): string => {
  const timestamp = Date.now();
  const randomBytes = crypto.randomBytes(8).toString('hex');
  const extension = path.extname(originalName);
  return `${timestamp}-${randomBytes}${extension}`;
};

// File upload endpoint
app.post('/api/upload', upload.single('file'), async (req: Request, res: Response) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'No file provided' });
    }
    
    // Validate file size
    const maxSize = parseInt(process.env.MAX_FILE_SIZE || '5242880');
    if (req.file.size > maxSize) {
      return res.status(400).json({ 
        error: `File too large. Maximum size is ${maxSize / 1024 / 1024}MB` 
      });
    }
    
    // Generate secure filename
    const secureFilename = generateSecureFilename(req.file.originalname);
    
    // Get upload directory from environment
    const uploadDir = process.env.UPLOAD_DIR || path.join(process.cwd(), 'uploads');
    
    // Ensure upload directory exists
    await fs.mkdir(uploadDir, { recursive: true });
    
    // Save file to disk
    const filePath = path.join(uploadDir, secureFilename);
    await fs.writeFile(filePath, req.file.buffer);
    
    // Generate file ID for tracking
    const fileId = crypto.randomUUID();
    
    const response: UploadResponse = {
      fileId,
      filename: secureFilename,
      size: req.file.size,
      mimeType: req.file.mimetype,
      uploadedAt: new Date().toISOString()
    };
    
    res.status(201).json(response);
    
  } catch (error) {
    console.error('File upload error:', error);
    
    if (error instanceof Error && error.message.includes('Invalid file type')) {
      return res.status(400).json({ error: error.message });
    }
    
    res.status(500).json({ error: 'Failed to upload file' });
  }
});

Database Operations with Environment Variables

/**
 * User data endpoint
 * 
 * Handles user data operations with database connection using environment variables.
 * Implements proper connection pooling and error handling.
 * 
 * @example
 * GET /api/users/123
 * Response: { id: "123", name: "John Doe", email: "john@example.com" }
 */

import { Pool } from 'pg';

interface User {
  id: string;
  name: string;
  email: string;
  createdAt: string;
}

interface CreateUserRequest {
  name: string;
  email: string;
}

// Database connection pool using environment variables
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
  max: 20, // Maximum number of connections
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

// Validate user input
const validateCreateUserRequest = (body: any): CreateUserRequest => {
  const { name, email } = body;
  
  if (!name || typeof name !== 'string' || name.trim().length === 0) {
    throw new Error('Name is required and must be a non-empty string');
  }
  
  if (!email || typeof email !== 'string' || !email.includes('@')) {
    throw new Error('Valid email address is required');
  }
  
  return {
    name: name.trim(),
    email: email.trim().toLowerCase()
  };
};

// Get user by ID
app.get('/api/users/:id', async (req: Request, res: Response) => {
  const { id } = req.params;
  
  try {
    // Validate user ID
    if (!id || typeof id !== 'string') {
      return res.status(400).json({ error: 'Valid user ID is required' });
    }
    
    // Query database with parameterized query
    const query = 'SELECT id, name, email, created_at FROM users WHERE id = $1';
    const result = await pool.query(query, [id]);
    
    if (result.rows.length === 0) {
      return res.status(404).json({ error: 'User not found' });
    }
    
    const user: User = {
      id: result.rows[0].id,
      name: result.rows[0].name,
      email: result.rows[0].email,
      createdAt: result.rows[0].created_at.toISOString()
    };
    
    res.json(user);
    
  } catch (error) {
    console.error('Database query error:', error);
    res.status(500).json({ error: 'Failed to fetch user data' });
  }
});

// Create new user
app.post('/api/users', async (req: Request, res: Response) => {
  try {
    // Validate request body
    const userData = validateCreateUserRequest(req.body);
    
    // Check if email already exists
    const existingUser = await pool.query(
      'SELECT id FROM users WHERE email = $1',
      [userData.email]
    );
    
    if (existingUser.rows.length > 0) {
      return res.status(409).json({ error: 'Email already registered' });
    }
    
    // Insert new user with parameterized query
    const insertQuery = `
      INSERT INTO users (name, email, created_at) 
      VALUES ($1, $2, NOW()) 
      RETURNING id, name, email, created_at
    `;
    
    const result = await pool.query(insertQuery, [userData.name, userData.email]);
    
    const newUser: User = {
      id: result.rows[0].id,
      name: result.rows[0].name,
      email: result.rows[0].email,
      createdAt: result.rows[0].created_at.toISOString()
    };
    
    res.status(201).json(newUser);
    
  } catch (error) {
    console.error('Create user error:', error);
    
    if (error instanceof Error && error.message.includes('required')) {
      return res.status(400).json({ error: error.message });
    }
    
    res.status(500).json({ error: 'Failed to create user' });
  }
});

Environment Variables Setup

Create a .env file in the project root with the following structure:

# Server Configuration
PORT=6080
NODE_ENV=development

# External API Keys (hidden from client)
WEATHER_API_KEY=your_openweathermap_api_key
STRIPE_SECRET_KEY=your_stripe_secret_key
GOOGLE_MAPS_API_KEY=your_google_maps_api_key

# Database Configuration
DATABASE_URL=postgresql://username:password@localhost:5432/database_name

# File Upload Configuration
MAX_FILE_SIZE=5242880
UPLOAD_DIR=/path/to/secure/upload/directory

# Security Configuration
JWT_SECRET=your_jwt_secret_key
SESSION_SECRET=your_session_secret