Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
488 changes: 487 additions & 1 deletion README.md

Large diffs are not rendered by default.

50 changes: 50 additions & 0 deletions env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# ============================================
# Games API - Environment Variables
# ============================================
# Copy this file to .env and fill in your values
# cp env.example .env

# ============================================
# Server Configuration
# ============================================

# Server port (optional, default: 8060)
PORT=8060

# Node environment: development, production, or test (optional, default: development)
NODE_ENV=development

# ============================================
# Database Configuration
# ============================================

# MongoDB connection URI (REQUIRED)
# Format: mongodb://username:password@host:port/databaseName
# Example: mongodb://admin:password123@localhost:27017/logs
MONGODB=mongodb://username:password@host:port/databaseName

# ============================================
# WebSocket Configuration
# ============================================

# WebSocket broker URL for real-time log ingestion (REQUIRED)
# Format: ws://broker-url:port or wss://broker-url:port
# Example: ws://localhost:8080
BROKER_URL=ws://broker-url:port

# ============================================
# Redis Configuration (Optional)
# ============================================

# Redis host (optional, default: localhost)
REDIS_HOST=localhost

# Redis port (optional, default: 6379)
REDIS_PORT=6379

# Redis username (optional, default: admin)
REDIS_USERNAME=admin

# Redis password (optional, default: default)
REDIS_PASSWORD=default

9 changes: 5 additions & 4 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import dotenv from 'dotenv';
dotenv.config();

import { AppConfig } from '@config';
import Databases from '@databases';
import { router } from '@routes';
import cors from 'cors';
import express from 'express';
import { createServer } from 'http';
import cors from 'cors';
import { router } from '@routes';
import Databases from '@databases';
import { WebsocketClient } from './src/websocket/websocket';

const app: express.Application = express();
Expand All @@ -20,7 +21,7 @@ app.set('trust proxy', true);

app.use('/', router);

const port = process.env.PORT || 8060;
const port = AppConfig.server.port;
server.listen(port, () => {
// eslint-disable-next-line no-console
console.warn(`Server listening on port ${port}`);
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "games-api",
"version": "1.0.0",
"version": "1.0.1",
"description": "Games API",
"main": "index",
"scripts": {
Expand Down Expand Up @@ -32,6 +32,7 @@
"devDependencies": {
"@types/cors": "^2.8.18",
"@types/express": "^4.17.21",
"@types/node": "^24.10.1",
"@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^8.43.0",
"eslint": "^8.56.0",
Expand Down
145 changes: 145 additions & 0 deletions src/_config/app-configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* Application Configuration
*
* Centralized configuration management with environment variable validation.
* This module validates all required environment variables and provides
* type-safe access to configuration values.
*/

// Ensure dotenv is loaded before accessing process.env
import dotenv from 'dotenv';
dotenv.config();

interface AppConfig {
server: {
port: number;
nodeEnv: string;
};
database: {
mongodb: {
uri: string;
};
};
websocket: {
brokerUrl: string;
};
redis: {
host: string;
port: number;
username: string;
password: string;
};
}

/**
* Validates that a required environment variable is present.
* Throws an error with a descriptive message if the variable is missing.
*/
function validateRequiredEnv(
key: string,
value: string | undefined,
description?: string,
): string {
if (!value || value.trim() === '') {
const errorMessage = description
? `Missing required environment variable: ${key}\nDescription: ${description}\n\nPlease set ${key} in your .env file or environment.`
: `Missing required environment variable: ${key}\n\nPlease set ${key} in your .env file or environment.`;

throw new Error(errorMessage);
}
return value;
}

/**
* Gets an optional environment variable with a default value.
*/
function getOptionalEnv(key: string, defaultValue: string): string {
return process.env[key] || defaultValue;
}

/**
* Gets an optional numeric environment variable with a default value.
*/
function getOptionalNumberEnv(key: string, defaultValue: number): number {
const value = process.env[key];
if (value === undefined || value === '') {
return defaultValue;
}
const parsed = parseInt(value, 10);
if (isNaN(parsed)) {
console.warn(
`Warning: ${key} is not a valid number. Using default value: ${defaultValue}`,
);
return defaultValue;
}
return parsed;
}

/**
* Application configuration object with validated environment variables.
* This object is initialized immediately when the module is imported.
*
* @throws {Error} If any required environment variable is missing
*/
export const AppConfig: AppConfig = (() => {
const mongodbUri = validateRequiredEnv(
'MONGODB',
process.env.MONGODB,
'MongoDB connection URI (e.g., mongodb://username:password@host:port/databaseName)',
);

const brokerUrl = validateRequiredEnv(
'BROKER_URL',
process.env.BROKER_URL,
'WebSocket broker URL for real-time log ingestion (e.g., ws://broker-url:port)',
);

const port = getOptionalNumberEnv('PORT', 8060);
const nodeEnv = getOptionalEnv('NODE_ENV', 'development');
const redisHost = getOptionalEnv('REDIS_HOST', 'localhost');
const redisPort = getOptionalNumberEnv('REDIS_PORT', 6379);
const redisUsername = getOptionalEnv('REDIS_USERNAME', 'admin');
const redisPassword = getOptionalEnv('REDIS_PASSWORD', 'default');

return {
server: {
port,
nodeEnv,
},
database: {
mongodb: {
uri: mongodbUri,
},
},
websocket: {
brokerUrl,
},
redis: {
host: redisHost,
port: redisPort,
username: redisUsername,
password: redisPassword,
},
};
})();

/**
* Helper function to check if the application is running in production mode.
*/
export function isProduction(): boolean {
return AppConfig.server.nodeEnv === 'production';
}

/**
* Helper function to check if the application is running in development mode.
*/
export function isDevelopment(): boolean {
return AppConfig.server.nodeEnv === 'development';
}

/**
* Helper function to check if the application is running in test mode.
*/
export function isTest(): boolean {
return AppConfig.server.nodeEnv === 'test';
}
6 changes: 6 additions & 0 deletions src/_config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export {
AppConfig,
isDevelopment,
isProduction,
isTest,
} from './app-configuration';
5 changes: 3 additions & 2 deletions src/databases/mongodb/mongodb.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AppConfig } from '@config';
import { GameKeys, games } from '@constants/game_types';
import { MongoClient } from 'mongodb';

Expand All @@ -12,9 +13,9 @@ export class MongoDB {

constructor(uri?: string) {
// Ex.: 'mongodb://username:password@host:port/databaseName';
const _uri = process.env.MONGODB || '';
const _uri = uri ?? AppConfig.database.mongodb.uri;

this._client = new MongoClient(uri ?? _uri);
this._client = new MongoClient(_uri);
}

public static initialize(): MongoDB {
Expand Down
9 changes: 4 additions & 5 deletions src/websocket/websocket.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AppConfig } from '@config';
import { MongoDB } from '@databases';
import { LogQuickTacToe, WebSocketMessage } from '@models';
import { LogsRepository } from '@repositories';
Expand Down Expand Up @@ -31,12 +32,10 @@ export class WebsocketClient {

private async _initialize(uri?: string) {
try {
const _uri = process.env.BROKER_URL || '';
const _uri = uri ?? AppConfig.websocket.brokerUrl;
if (!this._client) {
console.log(
`Connecting to WebSocket at ${uri ?? _uri}/${this._channel}`,
);
this._client = new WebSocket(`${uri ?? _uri}/${this._channel}`);
console.log(`Connecting to WebSocket at ${_uri}/${this._channel}`);
this._client = new WebSocket(`${_uri}/${this._channel}`);

if (this._client) {
// Register client after connection opens
Expand Down
Loading