add docker

This commit is contained in:
fiatcode 2025-10-20 08:18:02 +07:00
parent dcb2b5ee53
commit e9d703ec7c
10 changed files with 316 additions and 25 deletions

17
.dockerignore Normal file
View file

@ -0,0 +1,17 @@
node_modules
npm-debug.log
.env
.git
.gitignore
README.md
.vscode
.idea
*.md
.DS_Store
dist
coverage
.github
*.test.js
testLoad.js
testRateLimit.js
quotes.db

View file

@ -1,3 +1,2 @@
NODE_ENV=development NODE_ENV=development
PORT=8080
UNSPLASH_ACCESS_KEY=your_unsplash_access_key_here UNSPLASH_ACCESS_KEY=your_unsplash_access_key_here

39
Dockerfile Normal file
View file

@ -0,0 +1,39 @@
FROM node:22-alpine
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install ALL dependencies (including devDependencies for build)
RUN npm ci
# Copy source code
COPY . .
# Build TypeScript
RUN npm run build
# Remove devDependencies after build to reduce image size
RUN npm prune --production
# Create a non-root user for security
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Change ownership
RUN chown -R nodejs:nodejs /app
# Switch to non-root user
USER nodejs
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:8080/health || exit 1
# Start the application
CMD ["node", "dist/index.js"]

View file

@ -41,26 +41,6 @@ npm run format
- ESLint - ESLint
- Prettier - Prettier
## Project Structure
```
kuwot-api-js/
├── src/
│ ├── index.ts # Entry point
│ ├── routes.ts # API routes
│ ├── types.d.ts # Type definitions
│ └── data/
│ ├── db.ts # Database logic
│ └── unsplash.ts # Unsplash API integration
├── .env.example # Example environment variables
├── .gitignore # Git ignore rules
├── package.json # Project metadata and scripts
├── tsconfig.json # TypeScript configuration
└── .vscode/
├── launch.json # Debug configuration
└── extensions.json # Recommended extensions
```
## Contributing ## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

29
compose.yaml Normal file
View file

@ -0,0 +1,29 @@
services:
quote-api:
build: .
restart: unless-stopped
ports:
- '8080:8080'
env_file:
- .env
volumes:
- ./quotes.db:/app/quotes.db:ro
healthcheck:
test:
[
'CMD',
'wget',
'--quiet',
'--tries=1',
'--spider',
'http://localhost:8080/health',
]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
logging:
driver: 'json-file'
options:
max-size: '10m'
max-file: '3'

View file

@ -10,6 +10,10 @@ import {
const app = express(); const app = express();
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
const hourlyLimiter = rateLimit({ const hourlyLimiter = rateLimit({
windowMs: 60 * 60 * 1000, windowMs: 60 * 60 * 1000,
max: 1000, max: 1000,
@ -35,7 +39,6 @@ app.get('/quotes/:id', onGetQuoteById);
app.get('/translations', onGetTranslations); app.get('/translations', onGetTranslations);
app.get('/images', onGetImages); app.get('/images', onGetImages);
const port = process.env.PORT ? Number(process.env.PORT) : 8080; app.listen(8080, () => {
app.listen(port, () => { console.log(`Server is running on http://localhost:8080`);
console.log(`Server is running on http://localhost:${port}`);
}); });

View file

@ -1,5 +1,5 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { getQuote, getRandomQuote, listTranslations } from './data/db.js'; import { getQuote, getRandomQuote, listTranslations } from './data/quote.js';
import { listRandomImages } from './data/unsplash.js'; import { listRandomImages } from './data/unsplash.js';
function onGetRandomQuote(_req: Request, res: Response): void { function onGetRandomQuote(_req: Request, res: Response): void {

195
testLoad.js Normal file
View file

@ -0,0 +1,195 @@
// Stress test script for Quote API
// Usage: node stress-test.js
const ENDPOINTS = [
'http://localhost:8080/quotes/random',
'http://localhost:8080/translations',
];
// Test configurations
const TESTS = [
{ name: 'Light Load', concurrent: 10, duration: 10 },
{ name: 'Medium Load', concurrent: 50, duration: 10 },
{ name: 'Heavy Load', concurrent: 100, duration: 10 },
{ name: 'Extreme Load', concurrent: 200, duration: 10 },
];
class StressTest {
constructor(endpoint, concurrent, duration) {
this.endpoint = endpoint;
this.concurrent = concurrent;
this.duration = duration;
this.results = {
total: 0,
success: 0,
failed: 0,
rateLimited: 0,
totalTime: 0,
responseTimes: [],
};
}
async makeRequest() {
const start = Date.now();
try {
const response = await fetch(this.endpoint);
const elapsed = Date.now() - start;
this.results.total++;
this.results.totalTime += elapsed;
this.results.responseTimes.push(elapsed);
if (response.status === 429) {
this.results.rateLimited++;
} else if (response.ok) {
this.results.success++;
} else {
this.results.failed++;
}
} catch (error) {
this.results.total++;
this.results.failed++;
}
}
async runWorker(stopTime) {
while (Date.now() < stopTime) {
await this.makeRequest();
}
}
async run() {
console.log(
`\n🚀 Starting test: ${this.concurrent} concurrent requests for ${this.duration}s`
);
console.log(` Endpoint: ${this.endpoint}`);
const startTime = Date.now();
const stopTime = startTime + this.duration * 1000;
// Launch concurrent workers
const workers = [];
for (let i = 0; i < this.concurrent; i++) {
workers.push(this.runWorker(stopTime));
}
await Promise.all(workers);
const actualDuration = (Date.now() - startTime) / 1000;
return this.calculateStats(actualDuration);
}
calculateStats(duration) {
const { total, success, failed, rateLimited, responseTimes } = this.results;
// Sort response times for percentile calculations
responseTimes.sort((a, b) => a - b);
const getPercentile = (p) => {
const index = Math.ceil((p / 100) * responseTimes.length) - 1;
return responseTimes[index] || 0;
};
return {
requestsPerSecond: (total / duration).toFixed(2),
totalRequests: total,
successful: success,
failed: failed,
rateLimited: rateLimited,
successRate: ((success / total) * 100).toFixed(2) + '%',
avgResponseTime: (this.results.totalTime / total).toFixed(2) + 'ms',
minResponseTime: Math.min(...responseTimes) + 'ms',
maxResponseTime: Math.max(...responseTimes) + 'ms',
p50: getPercentile(50) + 'ms',
p95: getPercentile(95) + 'ms',
p99: getPercentile(99) + 'ms',
};
}
printResults(stats) {
console.log('\n📊 Results:');
console.log(` Requests/sec: ${stats.requestsPerSecond}`);
console.log(` Total requests: ${stats.totalRequests}`);
console.log(` Successful: ${stats.successful}`);
console.log(` Failed: ${stats.failed}`);
console.log(` Rate limited: ${stats.rateLimited}`);
console.log(` Success rate: ${stats.successRate}`);
console.log(`\n⏱️ Response Times:`);
console.log(` Average: ${stats.avgResponseTime}`);
console.log(` Min: ${stats.minResponseTime}`);
console.log(` Max: ${stats.maxResponseTime}`);
console.log(` 50th percentile: ${stats.p50}`);
console.log(` 95th percentile: ${stats.p95}`);
console.log(` 99th percentile: ${stats.p99}`);
}
}
async function runAllTests() {
console.log('═══════════════════════════════════════════');
console.log(' API STRESS TEST');
console.log('═══════════════════════════════════════════');
const endpoint = ENDPOINTS[0]; // Test random quotes endpoint
const allResults = [];
for (const testConfig of TESTS) {
const test = new StressTest(
endpoint,
testConfig.concurrent,
testConfig.duration
);
console.log(`\n\n▶️ ${testConfig.name.toUpperCase()}`);
const stats = await test.run();
test.printResults(stats);
allResults.push({
name: testConfig.name,
...stats,
});
// Cool down period between tests
if (testConfig !== TESTS[TESTS.length - 1]) {
console.log('\n\n⏳ Cooling down for 5 seconds...');
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
// Print summary
console.log('\n\n═══════════════════════════════════════════');
console.log(' SUMMARY');
console.log('═══════════════════════════════════════════\n');
console.log('Test Name | Req/s | Success Rate | Avg Response');
console.log('-------------------|--------|--------------|-------------');
allResults.forEach((result) => {
const name = result.name.padEnd(18);
const rps = result.requestsPerSecond.padEnd(6);
const successRate = result.successRate.padEnd(12);
const avgTime = result.avgResponseTime.padEnd(12);
console.log(`${name} | ${rps} | ${successRate} | ${avgTime}`);
});
console.log('\n✅ All tests completed!');
}
// Check if endpoint is reachable before starting
async function checkEndpoint() {
try {
const response = await fetch(ENDPOINTS[0]);
if (!response.ok && response.status !== 429) {
console.error('❌ Server not responding correctly. Is it running?');
process.exit(1);
}
} catch (error) {
console.error('❌ Cannot reach server. Please start your server first.');
console.error(` URL: ${ENDPOINTS[0]}`);
process.exit(1);
}
}
// Run the tests
(async () => {
await checkEndpoint();
await runAllTests();
})();

29
testRateLimit.js Normal file
View file

@ -0,0 +1,29 @@
async function testRateLimit() {
const url = 'http://localhost:8080/quotes/random';
console.log('Testing burst limiter (50 requests in 10 seconds)...\n');
for (let i = 1; i <= 60; i++) {
try {
const response = await fetch(url);
const status = response.status;
const remaining = response.headers.get('RateLimit-Remaining');
const limit = response.headers.get('RateLimit-Limit');
if (status === 429) {
console.log(`❌ Request ${i}: RATE LIMITED!`);
const data = await response.text();
console.log(` Message: ${data}\n`);
} else {
console.log(`✓ Request ${i}: OK (${remaining}/${limit} remaining)`);
}
} catch (error) {
console.log(`❌ Request ${i}: Error - ${error.message}`);
}
// Small delay to see output clearly
await new Promise((resolve) => setTimeout(resolve, 50));
}
}
testRateLimit();