Skip to content

Commit 912d568

Browse files
authored
Merge pull request #5 from JawherKl/feature/5-implement-best-practices
implemented: Rate limiting, caching, input validation, streaming and …
2 parents ce31697 + 70779ce commit 912d568

11 files changed

+303
-5
lines changed

package-lock.json

+189-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
"dependencies": {
1414
"dotenv": "^16.4.7",
1515
"express": "^4.21.2",
16-
"openai": "^4.82.0"
16+
"express-rate-limit": "^7.5.0",
17+
"ioredis": "^5.4.2",
18+
"openai": "^4.82.0",
19+
"prom-client": "^15.1.3",
20+
"rate-limit-redis": "^4.2.0",
21+
"zod": "^3.24.1"
1722
}
1823
}

src/app.mjs

+5
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import express from "express";
22
import analyzeRoutes from "./routes/analyzeRoutes.mjs";
33
import searchRoutes from "./routes/searchRoutes.mjs";
44
import { errorHandler } from "./middlewares/errorHandler.mjs";
5+
import rateLimiter from "./middlewares/rateLimiter.mjs";
6+
import { trackRequests, metricsHandler } from "./middlewares/monitoring.mjs";
57

68
const app = express();
79
app.use(express.json());
810

911
app.use("/search", searchRoutes);
1012
app.use("/analyze", analyzeRoutes);
13+
app.use(rateLimiter);
14+
app.use(trackRequests);
15+
app.get("/metrics", metricsHandler);
1116

1217
app.use(errorHandler);
1318

src/config/redisConfig.mjs

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Redis from "ioredis";
2+
import { config } from "dotenv";
3+
4+
config();
5+
6+
const redis = new Redis({
7+
host: process.env.REDIS_HOST || "127.0.0.1",
8+
port: process.env.REDIS_PORT || 6379,
9+
});
10+
11+
export default redis;

src/middlewares/monitoring.mjs

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import client from "prom-client";
2+
3+
const collectDefaultMetrics = client.collectDefaultMetrics;
4+
collectDefaultMetrics();
5+
6+
const requestCount = new client.Counter({
7+
name: "api_requests_total",
8+
help: "Total API requests",
9+
});
10+
11+
const requestDuration = new client.Histogram({
12+
name: "api_response_time_seconds",
13+
help: "API response time in seconds",
14+
buckets: [0.1, 0.3, 0.5, 1, 2, 5]
15+
});
16+
17+
export function trackRequests(req, res, next) {
18+
requestCount.inc();
19+
const start = process.hrtime();
20+
21+
res.on("finish", () => {
22+
const duration = process.hrtime(start);
23+
requestDuration.observe(duration[0] + duration[1] / 1e9);
24+
});
25+
26+
next();
27+
}
28+
29+
export function metricsHandler(req, res) {
30+
res.set("Content-Type", client.register.contentType);
31+
client.register.metrics().then(metrics => res.end(metrics));
32+
}

src/middlewares/rateLimiter.mjs

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import rateLimit from "express-rate-limit";
2+
import RedisStore from "rate-limit-redis";
3+
import redis from "../config/redisConfig.mjs";
4+
5+
const rateLimiter = rateLimit({
6+
store: new RedisStore({
7+
sendCommand: (...args) => redis.call(...args),
8+
}),
9+
windowMs: 15 * 60 * 1000, // 15 minutes
10+
max: 100, // Max 100 requests per window
11+
handler: (req, res) => {
12+
res.status(429).json({ error: "Too many requests, please try again later." });
13+
},
14+
keyGenerator: (req) => req.ip, // Rate limit per IP
15+
});
16+
17+
export default rateLimiter;

src/middlewares/validateInput.mjs

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { z } from "zod";
2+
3+
export function validateSchema(schema) {
4+
return (req, res, next) => {
5+
const result = schema.safeParse(req.body);
6+
if (!result.success) {
7+
return res.status(400).json({ error: result.error.errors });
8+
}
9+
next();
10+
};
11+
}
12+
13+
export const searchSchema = z.object({
14+
query: z.string().min(1, "Query must not be empty"),
15+
model: z.string().optional(),
16+
});
17+
18+
export const analyzeSchema = z.object({
19+
text: z.string().min(1, "Text must not be empty"),
20+
});

0 commit comments

Comments
 (0)