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

View file

@ -1,5 +1,5 @@
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';
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();