Skip to content

Express / Node.js Practice #47

Description

@abzalimovrrr

Практические задания

Практическое заданиеКоды
1Установите Express.
mkdir my-app && cd my-app
npm init -y
npm install express
# index.js:
const express = require("express");
const app = express();
app.listen(3000, () => console.log("Server on :3000"));
2Создайте маршруты.
const express = require("express");
const app = express();

app.get("/", (req, res) => res.send("Home"));
app.get("/users/:id", (req, res) => {
res.json({ id: req.params.id, name: "Alice" });
});
app.post("/users", (req, res) => res.status(201).send("Created"));
app.put("/users/:id", (req, res) => res.send("Updated"));
app.delete("/users/:id", (req, res) => res.send("Deleted"));

3Напишите middleware.
const logger = (req, res, next) => {
  console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
  next();
};

const auth = (req, res, next) => {
const token = req.headers.authorization;
if (!token) return res.status(401).json({ error: "Unauthorized" });
req.user = { id: 1 }; // добавляем данные в req
next();
};

app.use(logger); // глобальный middleware
app.use("/api", auth); // только для /api

4Парсинг тела запроса.
app.use(express.json());  // JSON
app.use(express.urlencoded({ extended: true }));  // формы

app.post("/api/data", (req, res) => {
console.log(req.body); // распарсенные данные
res.json({ received: req.body });
});

5Error middleware.
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: err.message || "Internal Server Error",
    ...(process.env.NODE_ENV === "development" && { stack: err.stack })
  });
});

// async handler wrapper:
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);

6Разделение маршрутов.
// routes/users.js
const router = require("express").Router();

router.get("/", async (req, res) => {
const users = await db.findMany();
res.json(users);
});
router.post("/", async (req, res) => {
const user = await db.create(req.body);
res.status(201).json(user);
});
module.exports = router;

// app.js
app.use("/api/users", require("./routes/users"));

7express.static.
app.use(express.static("public"));
// файлы из public/ доступны по /
// public/style.css -> http://localhost:3000/style.css

app.use("/static", express.static(__dirname + "/public"));
// префикс /static

// multiple folders:
app.use(express.static("public"));
app.use(express.static("uploads"));

8Настройка CORS.
npm install cors
const cors = require("cors");

app.use(cors()); // разрешить всё

app.use(cors({
origin: ["https://myapp.com", "http://localhost:5173"],
methods: ["GET", "POST", "PUT", "DELETE"],
credentials: true,
maxAge: 86400
}));

9Безопасность.
npm install helmet
const helmet = require("helmet");
app.use(helmet());
// Добавляет заголовки:
// X-Content-Type-Options: nosniff
// X-Frame-Options: SAMEORIGIN
// Strict-Transport-Security
// X-XSS-Protection
// Content-Security-Policy
10Morgan logger.
npm install morgan
const morgan = require("morgan");

app.use(morgan("dev")); // :method :url :status :response-time ms
app.use(morgan("combined")); // Apache combined format
app.use(morgan("short"));
app.use(morgan(":method :url :status :res[content-length] - :response-time ms"));

11EJS шаблоны.
npm install ejs
app.set("view engine", "ejs");

// views/index.ejs
// <h1><%= title %></h1>
// <ul><% users.forEach(u => { %>
// <li><%= u.name %></li>
// <% }) %></ul>

app.get("/", (req, res) => {
res.render("index", {
title: "Users",
users: [{ name: "Alice" }, { name: "Bob" }]
});
});

12Загрузка файлов.
npm install multer
const multer = require("multer");
const upload = multer({ dest: "uploads/" });

app.post("/upload", upload.single("file"), (req, res) => {
console.log(req.file); // { fieldname, originalname, path, size }
res.json({ file: req.file });
});

// Multiple files:
app.post("/uploads", upload.array("files", 5), (req, res) => {
res.json({ files: req.files });
});

13Валидация данных.
npm install express-validator
const { body, validationResult } = require("express-validator");

app.post("/users",
body("email").isEmail().normalizeEmail(),
body("name").isLength({ min: 2, max: 50 }).trim(),
body("age").isInt({ min: 18 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// создать пользователя
}
);

14Лимит запросов.
npm install express-rate-limit
const rateLimit = require("express-rate-limit");

const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 минут
max: 100, // 100 запросов за окно
message: { error: "Too many requests, try later" },
standardHeaders: true,
legacyHeaders: false
});
app.use("/api", limiter);

15Сжатие ответов.
npm install compression
const compression = require("compression");
app.use(compression());
// Автоматически сжимает ответы (gzip/brotli)
// Уменьшает размер до 70%
// Не сжимает маленькие ответы (< 1KB)
16dotenv.
npm install dotenv
// .env
PORT=3000
DB_URL=mongodb://localhost:27017/mydb
JWT_SECRET=mysecret

// app.js
require("dotenv").config();
const port = process.env.PORT || 3000;
const dbUrl = process.env.DB_URL;

app.listen(port);

17Стандартные ответы.
// GET /users — 200 OK
// POST /users — 201 Created
// GET /users/:id — 200
// PUT /users/:id — 200
// DELETE /users/:id — 204 No Content

res.status(200).json({ data });
res.status(201).location(/users/${id}).json(user);
res.status(204).send(); // без тела
res.status(400).json({ error: "Bad request" });
res.status(404).json({ error: "Not found" });
res.status(500).json({ error: "Internal" });

18Цепочки ответов.
res
  .status(201)
  .set("X-Custom-Header", "value")
  .cookie("token", "jwt", { httpOnly: true, maxAge: 3600000 })
  .redirect("/users")
  // или .json({ id: 1 })
  // или .send("Created")
19Свойства req.
app.use((req, res, next) => {
  console.log(req.params);  // :id, :name
  console.log(req.query);  // ?page=1&limit=10
  console.log(req.body);   // POST тело
  console.log(req.headers);  // заголовки
  console.log(req.ip);     // IP клиента
  console.log(req.path);   // /api/users
  console.log(req.method); // GET, POST
  console.log(req.hostname);
  next();
});
20Методы res.
res.json({ data });  // JSON ответ
res.send("<h1>Hello</h1>");  // строка/HTML
res.sendFile(__dirname + "/public/hello.html");
res.redirect("https://example.com");
res.redirect(301, "/new-path");
res.cookie("name", "value", { httpOnly: true });
res.clearCookie("name");
res.set("X-Custom", "value");
res.type("application/json");
21Async/await.
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

app.get("/users", asyncHandler(async (req, res) => {
const users = await User.find();
res.json(users);
}));

// или с try/catch:
app.get("/users", async (req, res, next) => {
try {
const users = await User.find();
res.json(users);
} catch (err) {
next(err);
}
});

22Mongoose модель.
npm install mongoose
const mongoose = require("mongoose");
mongoose.connect(process.env.DB_URL);

const userSchema = new mongoose.Schema({
name: { type: String, required: true, trim: true },
email: { type: String, required: true, unique: true, lowercase: true },
age: { type: Number, min: 18 },
createdAt: { type: Date, default: Date.now }
});

module.exports = mongoose.model("User", userSchema);

23Full CRUD.
const User = require("./models/User");

// Create
const user = await User.create(req.body);

// Read
const users = await User.find({ age: { $gte: 18 } }).sort("-createdAt").limit(10);
const user = await User.findById(id);

// Update
const user = await User.findByIdAndUpdate(id, req.body, { new: true, runValidators: true });

// Delete
await User.findByIdAndDelete(id);

24pg клиент.
npm install pg
const { Pool } = require("pg");
const pool = new Pool({
  connectionString: process.env.DB_URL
});

app.get("/users", async (req, res) => {
const result = await pool.query("SELECT * FROM users WHERE active = $1", [true]);
res.json(result.rows);
});

25Prisma схема.
// schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DB_URL")
}
model User {
  id        Int      @id @default(autoincrement())
  name      String
  email     String   @unique
  posts     Post[]
}
model Post {
  id        Int      @id @default(autoincrement())
  title     String
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
}

// app.js
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();

const users = await prisma.user.findMany({
include: { posts: { select: { title: true } } }
});

26JSON Web Tokens.
npm install jsonwebtoken bcrypt
const jwt = require("jsonwebtoken");
const bcrypt = require("bcrypt");

// Регистрация
const hashedPass = await bcrypt.hash(password, 10);
await User.create({ email, password: hashedPass });

// Логин
const user = await User.findOne({ email });
const match = await bcrypt.compare(password, user.password);
if (!match) return res.status(401).json({ error: "Invalid credentials" });

const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: "7d" });
res.json({ token });

// Проверка
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userId = decoded.userId;

27Стратегии аутентификации.
npm install passport passport-jwt
const passport = require("passport");
const { Strategy: JwtStrategy, ExtractJwt } = require("passport-jwt");

const opts = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET
};

passport.use(new JwtStrategy(opts, async (payload, done) => {
try {
const user = await User.findById(payload.userId);
if (user) return done(null, user);
return done(null, false);
} catch (err) {
return done(err, false);
}
}));

// Защита маршрута:
app.get("/profile", passport.authenticate("jwt", { session: false }), (req, res) => {
res.json(req.user);
});

28Сессии.
npm install express-session
const session = require("express-session");

app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true, maxAge: 24 * 60 * 60 * 1000 }
}));

app.get("/login", (req, res) => {
req.session.userId = 1;
res.send("Logged in");
});
app.get("/profile", (req, res) => {
if (!req.session.userId) return res.status(401).send("Unauthorized");
res.send(User ${req.session.userId});
});

29Socket.IO чат.
npm install socket.io
const { Server } = require("socket.io");
const io = new Server(httpServer, { cors: { origin: "*" } });

io.on("connection", (socket) => {
console.log(User connected: ${socket.id});

socket.on("message", (data) => {
io.emit("message", { user: socket.id, text: data });
});

socket.on("disconnect", () => {
console.log(User disconnected: ${socket.id});
});
});

30HTTP/2 сервер.
const http2 = require("http2");
const fs = require("fs");

const server = http2.createSecureServer({
key: fs.readFileSync("server.key"),
cert: fs.readFileSync("server.crt")
});

server.on("stream", (stream, headers) => {
stream.respond({ ":status": 200 });
stream.end("Hello HTTP/2");
});
server.listen(3000);

31Cluster модуль.
const cluster = require("cluster");
const os = require("os");

if (cluster.isMaster) {
const numCPUs = os.cpus().length;
console.log(Master ${process.pid} spawning ${numCPUs} workers);
for (let i = 0; i < numCPUs; i++) cluster.fork();
cluster.on("exit", (worker) => cluster.fork());
} else {
const app = require("./app");
app.listen(3000);
console.log(Worker ${process.pid} started);
}

32PM2 для прода.
npm install -g pm2
pm2 start app.js -i max  # все ядра
pm2 start app.js --name "my-api" -i 4
pm2 list
pm2 logs
pm2 monit
pm2 restart all
pm2 reload all  # без downtime
pm2 stop all
pm2 delete all

ecosystem.config.js

module.exports = {
apps: [{
name: "api",
script: "app.js",
instances: "max",
exec_mode: "cluster",
env: { NODE_ENV: "production" }
}]
};

33Эндпоинт здоровья.
app.get("/health", (req, res) => {
  res.json({
    status: "ok",
    uptime: process.uptime(),
    timestamp: new Date().toISOString(),
    memory: process.memoryUsage()
  });
});

// readiness probe (готов к трафику)
app.get("/ready", async (req, res) => {
try {
await db.raw("SELECT 1");
res.json({ status: "ready" });
} catch (e) {
res.status(503).json({ status: "not ready" });
}
});

34Корректное завершение.
const server = app.listen(3000);

process.on("SIGTERM", async () => {
console.log("SIGTERM received. Shutting down gracefully...");
server.close(async () => {
await db.destroy(); // закрыть соединения с БД
console.log("Server closed");
process.exit(0);
});
setTimeout(() => {
console.error("Forced shutdown");
process.exit(1);
}, 10000); // таймаут 10 сек
});

35Интеграционные тесты.
npm install -D jest supertest
const request = require("supertest");
const app = require("../app");

describe("GET /users", () => {
it("returns users list", async () => {
const res = await request(app)
.get("/api/users")
.set("Authorization", "Bearer token")
.expect(200);
expect(res.body).toBeInstanceOf(Array);
});

it("returns 401 without auth", async () => {
await request(app).get("/api/users").expect(401);
});
});

36Swagger документация.
npm install swagger-jsdoc swagger-ui-express
const swaggerJsDoc = require("swagger-jsdoc");
const swaggerUi = require("swagger-ui-express");

const options = {
definition: {
openapi: "3.0.0",
info: { title: "API", version: "1.0.0" }
},
apis: ["./routes/*.js"]
};
const specs = swaggerJsDoc(options);
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs));

// В routes:
/**

  • @swagger
  • /users:
  • get:
  • summary: Get all users
    
  • responses:
    
  •   200:
    
  •     description: Users list
    

*/

37Стандартный обработчик.
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}

app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.statusCode.toString().startsWith("4") ? "fail" : "error";

if (process.env.NODE_ENV === "development") {
res.status(err.statusCode).json({
status: err.status,
message: err.message,
stack: err.stack
});
} else {
res.status(err.statusCode).json({
status: err.status,
message: err.isOperational ? err.message : "Something went wrong"
});
}
});

38Версионирование API.
// Через URL:
app.use("/api/v1", v1Router);
app.use("/api/v2", v2Router);

// Через заголовки:
app.use((req, res, next) => {
const version = req.headers["accept-version"];
if (version === "2") req.version = 2;
else req.version = 1;
next();
});

// Структура:
// routes/
// v1/
// users.js
// v2/
// users.js

39Пагинация.
app.get("/users", async (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 20;
  const skip = (page - 1) * limit;

const [users, total] = await Promise.all([
User.find().skip(skip).limit(limit),
User.countDocuments()
]);

res.json({
data: users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1
}
});
});

40Фильтрация.
app.get("/users", async (req, res) => {
  const filter = {};
  if (req.query.name) filter.name = { $regex: req.query.name, $options: "i" };
  if (req.query.minAge) filter.age = { $gte: parseInt(req.query.minAge) };
  if (req.query.active) filter.active = req.query.active === "true";

const sort = {};
if (req.query.sort) {
const fields = req.query.sort.split(",");
fields.forEach(f => {
if (f.startsWith("-")) sort[f.slice(1)] = -1;
else sort[f] = 1;
});
}

const users = await User.find(filter).sort(sort);
res.json(users);
});

41Jest настройка.
// package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  },
  "jest": {
    "testEnvironment": "node",
    "coveragePathIgnorePatterns": ["/node_modules/"]
  }
}

// sum.test.js
test("adds 1 + 2 = 3", () => {
expect(1 + 2).toBe(3);
});

42Моки в Jest.
const mockUser = { id: 1, name: "Alice" };
jest.mock("../models/User", () => ({
  find: jest.fn().mockResolvedValue([mockUser]),
  findById: jest.fn().mockResolvedValue(mockUser),
  create: jest.fn().mockResolvedValue(mockUser)
}));

const User = require("../models/User");
test("get users", async () => {
const res = await request(app).get("/api/users");
expect(User.find).toHaveBeenCalled();
expect(res.body).toEqual([mockUser]);
});

43Spies.
const logger = require("../utils/logger");
const spy = jest.spyOn(logger, "info");

app.get("/test", (req, res) => {
logger.info("Test endpoint called");
res.json({ ok: true });
});

test("logs on /test", async () => {
await request(app).get("/test");
expect(spy).toHaveBeenCalledWith("Test endpoint called");
spy.mockRestore();
});

44Интеграционные тесты с БД.
npm install -D mongodb-memory-server
const { MongoMemoryServer } = require("mongodb-memory-server");

let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
await mongoose.connect(mongoServer.getUri());
});

afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});

beforeEach(async () => {
await User.deleteMany({}); // чистка между тестами
});

45ESLint конфиг.
npm install -D eslint
// .eslintrc.json
{
  "extends": ["eslint:recommended"],
  "env": {
    "node": true,
    "es2022": true,
    "jest": true
  },
  "rules": {
    "no-console": "warn",
    "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
    "prefer-const": "error"
  }
}
46Кэш с Redis.
npm install ioredis
const Redis = require("ioredis");
const redis = new Redis();

const cacheMiddleware = (ttl = 300) => async (req, res, next) => {
const key = cache:${req.originalUrl};
const cached = await redis.get(key);
if (cached) return res.json(JSON.parse(cached));

res.originalJson = res.json.bind(res);
res.json = (data) => {
redis.setex(key, ttl, JSON.stringify(data));
res.originalJson(data);
};
next();
};

app.get("/users", cacheMiddleware(), async (req, res) => {
const users = await User.find();
res.json(users);
});

47Bull очереди.
npm install bull
const Queue = require("bull");
const emailQueue = new Queue("email", process.env.REDIS_URL);

// Producer
app.post("/register", async (req, res) => {
const user = await User.create(req.body);
await emailQueue.add({ userId: user.id, template: "welcome" });
res.status(201).json(user);
});

// Worker
emailQueue.process(async (job) => {
const { userId, template } = job.data;
const user = await User.findById(userId);
await sendEmail(user.email, template);
return { sent: true };
});

48События Node.js.
const EventEmitter = require("events");
const myEmitter = new EventEmitter();

// Подписка
myEmitter.on("user:created", (user) => {
console.log(User created: ${user.id});
sendWelcomeEmail(user);
logToAnalytics(user);
});

// Использование в приложении:
app.post("/users", async (req, res) => {
const user = await User.create(req.body);
myEmitter.emit("user:created", user);
res.status(201).json(user);
});

49Readable/Writable streams.
const fs = require("fs");
const zlib = require("zlib");

// Чтение и сжатие файла
fs.createReadStream("input.log")
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream("input.log.gz"));

// Stream через Express
app.get("/download", (req, res) => {
const stream = fs.createReadStream("large-file.zip");
stream.pipe(res);
});

50Доп. методы ответа.
// res.download — отправить файл как загрузку:
app.get("/download", (req, res) => {
  const file = __dirname + "/files/report.pdf";
  res.download(file, "report.pdf", (err) => {
    if (err) console.error("Download failed:", err);
  });
});

// res.format — content negotiation:
app.get("/api/data", (req, res) => {
res.format({
"text/plain": () => res.send("Hello"),
"text/html": () => res.send("<h1>Hello</h1>"),
"application/json": () => res.json({ message: "Hello" }),
default: () => res.status(406).send("Not Acceptable")
});
});

// res.jsonp — JSONP для кросс-доменных запросов:
app.get("/api/data", (req, res) => {
res.jsonp({ message: "Hello" });
// GET /api/data?callback=myFunc -> myFunc({ message: "Hello" })
});

// res.location + res.links:
res.location("/users/123");
res.set("Link", "</users?page=2>; rel="next"");

51Профилирование Node.js.
npm install -g clinic
npm install -g autocannon

1. Clinic Doctor — общий анализ:

clinic doctor -- node app.js

Запустить нагрузку: autocannon -c 10 -d 10 http://localhost:3000

Остановить, открыть clinic-*.html

2. Clinic Flame — флеймграф:

clinic flame -- node app.js

3. Clinic Bubbleprof — асинхронные задержки:

clinic bubbleprof -- node app.js

4. Профилирование встроенными средствами:

node --prof app.js

Создаст isolate-*.log

node --prof-process isolate-*.log > processed.txt

5. Chrome DevTools:

node --inspect-brk app.js

chrome://inspect -> CPU Profiler

6. 0x — флеймграф:

npm install -g 0x
0x app.js

52Оптимизация пайплайна.
// Order matters — тяжелые middleware только на нужные роуты

// ПЛОХО:
app.use(express.json());
app.use(cors());
app.use(morgan("dev"));
app.use(rateLimit());
app.use(helmet());
app.use("/api/users", userRoutes); // все middleware на каждый запрос

// ХОРОШО:
app.use(helmet());
app.use(cors());
app.use(morgan("dev"));

// Группировка middleware по роутам:
const apiMiddleware = [express.json(), rateLimit(), auth];
app.use("/api", apiMiddleware);

// Conditional:
app.use((req, res, next) => {
if (req.path.startsWith("/webhook")) return express.raw({ type: "/" })(req, res, next);
next();
});

53Стриминг ответов.
app.get("/stream", (req, res) => {
  res.setHeader("Content-Type", "text/plain");
  res.setHeader("Transfer-Encoding", "chunked");

let count = 0;
const interval = setInterval(() => {
count++;
res.write(Chunk ${count}\n);
if (count === 10) {
clearInterval(interval);
res.end();
}
}, 200);
});

// Стриминг большого JSON массива:
app.get("/users/stream", async (req, res) => {
const cursor = User.find().cursor();
res.setHeader("Content-Type", "application/json");
res.write("[");
let first = true;
cursor.on("data", (doc) => {
res.write(first ? JSON.stringify(doc) : "," + JSON.stringify(doc));
first = false;
});
cursor.on("end", () => { res.write("]"); res.end(); });
});

54SSE в Express.
app.get("/events", (req, res) => {
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");
  res.flushHeaders();

const sendEvent = (data, event) => {
if (event) res.write(event: ${event}\n);
res.write(data: ${JSON.stringify(data)}\n\n);
};

sendEvent({ message: "Connected" }, "connected");

const interval = setInterval(() => {
sendEvent({ time: new Date().toISOString(), value: Math.random() }, "tick");
}, 3000);

req.on("close", () => {
clearInterval(interval);
res.end();
});
});

// Клиент:
// const evtSource = new EventSource("/events");
// evtSource.addEventListener("tick", (e) => console.log(JSON.parse(e.data)));

55GraphQL с Express.
npm install express-graphql graphql
const { graphqlHTTP } = require("express-graphql");
const { buildSchema } = require("graphql");

const schema = buildSchema(type User { id: ID! name: String! email: String! posts: [Post] } type Post { title: String! content: String! } type Query { users: [User] user(id: ID!): User } type Mutation { createUser(name: String!, email: String!): User });

const root = {
users: async () => await User.find().populate("posts"),
user: async ({ id }) => await User.findById(id),
createUser: async ({ name, email }) => await User.create({ name, email })
};

app.use("/graphql", graphqlHTTP({
schema,
rootValue: root,
graphiql: true // GraphQL IDE
}));

56Стриминг файлов.
const fs = require("fs");
const path = require("path");

app.get("/download/:file", (req, res) => {
const filePath = path.join(__dirname, "files", req.params.file);
const stat = fs.statSync(filePath);

res.writeHead(200, {
"Content-Type": "application/octet-stream",
"Content-Length": stat.size,
"Content-Disposition": attachment; filename="${req.params.file}"
});

const stream = fs.createReadStream(filePath);
stream.pipe(res);

let bytes = 0;
stream.on("data", (chunk) => {
bytes += chunk.length;
const pct = ((bytes / stat.size) * 100).toFixed(1);
console.log(Progress: ${pct}%);
});
});

// Асинхронная загрузка с multer:
const upload = multer({ dest: "uploads/", limits: { fileSize: 500 * 1024 * 1024 } });
app.post("/upload", upload.single("file"), (req, res) => {
res.json({ path: req.file.path, size: req.file.size });
});

57Прокси middleware.
npm install http-proxy-middleware
const { createProxyMiddleware } = require("http-proxy-middleware");

// Прокси на другой сервер
app.use("/api/users", createProxyMiddleware({
target: "http://users-service:3000",
changeOrigin: true,
pathRewrite: { "^/api/users": "/users" }
}));

// Балансировка
app.use("/api", createProxyMiddleware({
target: "http://backend:3000",
router: {
"/api/v1": "http://v1-backend:3000",
"/api/v2": "http://v2-backend:3001"
}
}));

// WebSocket прокси
app.use("/ws", createProxyMiddleware({
target: "ws://ws-server:4000",
ws: true
}));

58Условные middleware.
const unless = (path, middleware) => (req, res, next) => {
  if (path === req.path || req.path.startsWith(path)) return next();
  return middleware(req, res, next);
};

// Примеры:
app.use(unless("/health", rateLimit()));
app.use(unless("/webhook", express.json()));

// Или через функцию:
app.use((req, res, next) => {
req.skipLogging = req.path === "/health" || req.path === "/metrics";
next();
});

// Skip на основе метода:
const skipMethods = (methods, middleware) => (req, res, next) => {
if (methods.includes(req.method)) return next();
return middleware(req, res, next);
};
app.use(skipMethods(["GET", "HEAD"], express.json()));

59Joi схемы.
npm install joi
const Joi = require("joi");

const userSchema = Joi.object({
name: Joi.string().min(2).max(50).trim().required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(/^(?=.[a-z])(?=.[A-Z])(?=.*\d)/).required(),
age: Joi.number().integer().min(18).max(120).optional(),
role: Joi.string().valid("user", "admin", "moderator").default("user")
});

const validate = (schema) => (req, res, next) => {
const { error, value } = schema.validate(req.body, { abortEarly: false, stripUnknown: true });
if (error) {
const errors = error.details.map((d) => ({ field: d.path.join("."), message: d.message }));
return res.status(422).json({ errors });
}
req.body = value;
next();
};

app.post("/users", validate(userSchema), async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
});

60Тайминг запросов.
app.use((req, res, next) => {
  const start = process.hrtime.bigint();
  const originalEnd = res.end.bind(res);

res.end = (...args) => {
const duration = Number(process.hrtime.bigint() - start) / 1e6;
res.set("X-Response-Time", ${duration.toFixed(2)}ms);
console.log(${req.method} ${req.originalUrl} - ${duration.toFixed(2)}ms);
originalEnd(...args);
};

next();
});

// Мониторинг медленных запросов:
app.use((req, res, next) => {
const timer = setTimeout(() => {
console.warn(SLOW REQUEST: ${req.method} ${req.originalUrl} &gt; 5s);
}, 5000);
res.on("finish", () => clearTimeout(timer));
next();
});

61Helmet настройка.
const helmet = require("helmet");

app.use(helmed({
contentSecurityPolicy: {
directives: {
defaultSrc: [""self""],
scriptSrc: [""self"", ""unsafe-inline"", "https://cdn.example.com"],
styleSrc: [""self"", ""unsafe-inline"", "https://fonts.googleapis.com"],
imgSrc: [""self"", "data:", "https:"],
connectSrc: [""self"", "https://api.example.com"],
fontSrc: [""self"", "https://fonts.gstatic.com"],
objectSrc: [""none""],
frameAncestors: [""self""],
upgradeInsecureRequests: []
}
},
crossOriginEmbedderPolicy: { policy: "require-corp" },
crossOriginOpenerPolicy: { policy: "same-origin" },
crossOriginResourcePolicy: { policy: "same-origin" },
dnsPrefetchControl: { allow: false },
expectCt: { maxAge: 86400, enforce: true },
frameguard: { action: "deny" },
hidePoweredBy: true,
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
ieNoOpen: true,
noSniff: true,
permittedCrossDomainPolicies: { permittedPolicies: "none" },
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
xssFilter: true
}));

62CSRF token.
npm install csurf
const csurf = require("csurf");
const cookieParser = require("cookie-parser");

app.use(cookieParser());
app.use(csurf({ cookie: { httpOnly: true, sameSite: "strict" } }));

app.get("/form", (req, res) => {
res.send(&lt;form action="/transfer" method="POST"&gt; &lt;input type="hidden" name="_csrf" value="${req.csrfToken()}"&gt; &lt;input type="text" name="amount"&gt; &lt;button type="submit"&gt;Send&lt;/button&gt; &lt;/form&gt;);
});

// Для SPA — передача токена через cookie:
app.get("/api/csrf-token", (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});

// Или с double submit cookie:
const crypto = require("crypto");
app.use((req, res, next) => {
if (["POST", "PUT", "DELETE", "PATCH"].includes(req.method)) {
const cookieToken = req.cookies["csrf-token"];
const headerToken = req.headers["x-csrf-token"];
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return res.status(403).json({ error: "Invalid CSRF token" });
}
}
next();
});

63HPP защита.
npm install hpp
const hpp = require("hpp");

// Защита от загрязнения параметров:
// ?id=1&id=2&id=3 -> req.query.id = ["1", "2", "3"]

app.use(hpp({
whitelist: ["sort", "fields"] // параметры, где дубликаты разрешены
}));

// Без hpp:
app.use((req, res, next) => {
for (const [key, value] of Object.entries(req.query)) {
if (Array.isArray(value) && !["sort", "fields", "filter"].includes(key)) {
return res.status(400).json({ error: Parameter ${key} duplicated });
}
}
next();
});

64Санитизация ввода.
npm install xss
const xss = require("xss");
const striptags = require("striptags");

// Middleware санитизации:
app.use((req, res, next) => {
if (req.body) {
for (const key of Object.keys(req.body)) {
if (typeof req.body[key] === "string") {
req.body[key] = req.body[key].trim();
req.body[key] = xss(req.body[key]); // удалить XSS
if (key === "email") req.body[key] = req.body[key].toLowerCase();
}
}
}
if (req.query) {
for (const key of Object.keys(req.query)) {
if (typeof req.query[key] === "string") {
req.query[key] = striptags(req.query[key]);
}
}
}
next();
});

// Санитизация HTML:
const sanitizeHtml = require("sanitize-html");
app.post("/comment", (req, res) => {
const clean = sanitizeHtml(req.body.html, {
allowedTags: ["b", "i", "em", "strong", "a"],
allowedAttributes: { a: ["href"] },
allowedIframeHostnames: []
});
res.json({ clean });
});

65Защита от SQL injection.
// ПЛОХО — конкатенация:
// const result = await pool.query(`SELECT * FROM users WHERE id = ${req.params.id}`);

// ХОРОШО — параметризованные запросы:
const result = await pool.query("SELECT * FROM users WHERE id = $1", [req.params.id]);

// С Sequelize:
const user = await User.findOne({ where: { id: req.params.id } }); // безопасно

// С Prisma:
const user = await prisma.user.findUnique({ where: { id: parseInt(req.params.id) } });

// Валидация ID:
if (!/^\d+$/.test(req.params.id)) {
return res.status(400).json({ error: "Invalid ID format" });
}

// ORM защита:
const user = await User.findByPk(req.params.id);
// Sequelize/Knex/Prisma экранируют автоматически

66Защита от XSS.
// Content Security Policy (через helmet):
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"]
  }
}));

// Экранирование на сервере:
const escapeHtml = (str) => str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);

app.post("/profile", (req, res) => {
const safeName = escapeHtml(req.body.name);
res.send(&lt;h1&gt;Welcome ${safeName}&lt;/h1&gt;);
});

67Аудит заголовков.
// Проверка заголовков ответа:
app.use((req, res, next) => {
  res.on("finish", () => {
    const requiredHeaders = [
      "strict-transport-security",
      "x-content-type-options",
      "x-frame-options",
      "x-xss-protection",
      "content-security-policy",
      "referrer-policy"
    ];
    const missing = requiredHeaders.filter(h => !res.getHeader(h));
    if (missing.length > 0) {
      console.warn(`Missing security headers: ${missing.join(", ")}`);
    }
  });
  next();
});

// Инструмент для аудита:
// npx helmet-csp -u https://example.com
// npm install -g observatory-cli && observatory https://example.com

// Проверка через curl:
// curl -sI https://example.com | grep -i -E "^(strict|content|x-|referrer)"

68Брутфорс защита.
npm install express-brute
const ExpressBrute = require("express-brute");
const store = new ExpressBrute.MemoryStore();

const loginBruteForce = new ExpressBrute(store, {
freeRetries: 5,
minWait: 5000,
maxWait: 60000,
failCallback: (req, res, next, nextValidRequestDate) => {
res.status(429).json({
error: "Too many login attempts",
retryAfter: Math.ceil((nextValidRequestDate - Date.now()) / 1000)
});
}
});

app.post("/login", loginBruteForce.prevent, async (req, res) => {
// ... логика логина
});

// Rate limiting комбинация:
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
skipSuccessfulRequests: true,
keyGenerator: (req) => req.ip + ":" + req.body.email
});
app.post("/login", loginLimiter, async (req, res) => { ... });

69Безопасные куки.
// Настройка cookie:
res.cookie("session", token, {
  httpOnly: true,      // недоступен JS
  secure: true,        // только HTTPS
  sameSite: "strict",  // защита от CSRF
  maxAge: 7 * 24 * 60 * 60 * 1000,  // 7 дней
  path: "/",
  domain: ".example.com",
  signed: true         // подпись cookie
});

// Подписанные cookie:
app.use(cookieParser(process.env.COOKIE_SECRET));
// req.signedCookies вместо req.cookies

// Проверка:
app.use((req, res, next) => {
const token = req.signedCookies.session;
if (!token) return res.status(401).json({ error: "No session" });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (e) {
res.clearCookie("session");
return res.status(401).json({ error: "Invalid session" });
}
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions