@the-teacher/the-router
Version:
Simple router for Express.js, making routes and actions easy to manage.
530 lines (398 loc) • 17.4 kB
Markdown
# Часто задаваемые вопросы
## Зачем нужен этот роутер?
Express.js предоставляет базовый роутер, но его гибкость и отсутствие предопределённой структуры приложения часто приводят к несогласованным решениям в разных проектах.
Этот роутер является надстройкой над базовым роутером Express.js и:
- Задаёт чёткую структуру проекта
- Делает маршрутизацию интуитивно понятной
- Упрощает поддержку кода
- Способствует лучшей организации бизнес-логики
## Что общего с Ruby on Rails?
Роутер вдохновлён подходом Ruby on Rails к определению маршрутов:
```ts
// Rails-подобный синтаксис
root("pages/home");
get("/about", "pages/about");
resources("posts");
```
Поддерживаются все стандартные REST-действия:
- `index` - список ресурсов
- `show` - просмотр ресурса
- `new` - форма создания
- `create` - создание ресурса
- `edit` - форма редактирования
- `update` - обновление ресурса
- `destroy` - удаление ресурса
Метод `resources` автоматически создаёт все необходимые базовые маршруты:
```ts
resources("posts");
// Создаст маршруты:
// GET /posts -> posts/index
// GET /posts/new -> posts/new
// POST /posts -> posts/create
// GET /posts/:id -> posts/show
// GET /posts/:id/edit -> posts/edit
// PUT /posts/:id -> posts/update
// PATCH /posts/:id -> posts/update
// DELETE /posts/:id -> posts/destroy
```
## Что общего с Hanami?
Главное сходство с Hanami - это подход к организации действий (actions). В традиционных контроллерах часто накапливается много кода, что затрудняет их поддержку:
```ts
// Традиционный подход с контроллером
class PostsController {
index() {
/* ... */
}
show() {
/* ... */
}
create() {
/* ... */
}
// и т.д.
}
```
Как и в Hanami, этот роутер поощряет разделение действий на отдельные файлы:
```
src/
actions/
posts/
indexAction.ts // Только логика списка постов
showAction.ts // Только логика просмотра поста
createAction.ts // Только логика создания поста
```
Каждое действие - это отдельный модуль с единственной ответственностью:
```ts
// src/actions/posts/showAction.ts
export const perform = (req: Request, res: Response) => {
const { id } = req.params;
// Логика только для просмотра поста
};
```
## Как работает роутер?
1. Определяем маршруты:
```ts
// routes/index.ts
import { root, get, resources } from "@the-teacher/the-router";
root("pages/home");
get("/about", "pages/about");
resources("posts");
```
2. Создаём действия:
```
src/
actions/
pages/
homeAction.ts
aboutAction.ts
posts/
indexAction.ts
showAction.ts
// ...
```
3. Маршрутизация запроса:
```
GET /posts/123 ->
1. Находит маршрут posts/:id
2. Определяет действие posts#show
3. Выполняет src/actions/posts/showAction.ts
```
## Как начать использовать роутер?
1. Создайте структуру каталогов:
```
src/
actions/ # Каталог для действий
routes/ # Каталог для маршрутов
index.ts # Определения маршрутов
```
2. Определите маршруты:
```ts
// src/routes/index.ts
import { root, get, resources } from "@the-teacher/the-router";
root("pages/home");
resources("posts");
```
3. Создайте действия:
```ts
// src/actions/pages/homeAction.ts
export const perform = (req, res) => {
res.render("home");
};
```
4. Подключите роутер:
```ts
// src/index.ts
import express from "express";
import { getRouter } from "@the-teacher/the-router";
import "./routes";
const app = express();
app.use(getRouter());
app.listen(3000, () => {
console.log("Server is running on port 3000");
});
```
## Дополнительные возможности
### Группировка маршрутов
Маршруты можно группировать с помощью `scope`:
```ts
scope("admin", () => {
get("/dashboard", "admin/dashboard"); // /admin/dashboard
resources("posts"); // /admin/posts
resources("users"); // /admin/users
});
```
### Middleware
Middleware можно применять к отдельным маршрутам или группам:
```ts
get("/profile", [authenticate], "users/profile");
scope("admin", [authenticate, requireAdmin], () => {
// Все маршруты требуют аутентификации и прав админа
resources("users");
});
```
Если вы используете несколько middleware, рекомендуется заранее сформировать массив:
```ts
const permissionsMiddlewares = [authenticate, requireOwner, requireEditorRole];
get("/profile", permissionsMiddlewares, "users/profile");
```
### Регулярные выражения
Поддерживаются маршруты с регулярными выражениями:
```ts
get(/.*fly$/, "insects#list"); // Совпадёт с /butterfly, /dragonfly
```
### Порядок маршрутов
Порядок определения маршрутов важен:
```ts
// ✅ Правильно
get("/posts/featured", "posts#featured");
get("/posts/:id", "posts#show");
// ❌ Неправильно
get("/posts/:id", "posts#show"); // Перехватит /posts/featured
get("/posts/featured", "posts#featured"); // Никогда не сработает
```
### Можно ли использовать контроллеры?
Роутер специально спроектирован для работы с отдельными действиями. Действия располагаются в отдельных файлах в соответствующих директориях. Директории соответствуют контроллерам.
Контроллеры используются в логическом разделении кода, но не в физическом.
Это сделано для того, чтобы исключить чтение и понимание иногда сложной логики в файлах контроллеров, где из-за наличия множества действий их сложно читать и понимать.
Действие является отдельным модулем, который экспортирует функцию `perform`. Эта функция является обработчиком запроса пришедшего от `Express.js` и принимает те же параметры:
```ts
import { Request, Response } from "express";
export const perform = (req: Request, res: Response) => {
// req - объект запроса Express.js
// - req.params - параметры маршрута (/users/:id)
// - req.query - параметры запроса (?name=value)
// - req.body - тело запроса (для POST/PUT/PATCH)
// - req.headers - заголовки запроса
```
### Как организовать повторно используемую логику?
Создайте общие middleware или утилиты:
```ts
// middleware/auth.ts
export const authenticate = (req, res, next) => {
// Общая логика аутентификации
};
// validations/postsValidations.ts
export const validatePost = (data) => {
// Общая логика валидации
};
```
### Как тестировать действия?
Каждое действие - это отдельный модуль, что упрощает тестирование:
```ts
import { perform } from "../actions/posts/showAction";
test("show action", () => {
const req = { params: { id: "123" } };
const res = { json: jest.fn() };
perform(req, res);
expect(res.json).toHaveBeenCalledWith(/* ... */);
});
```
## Устройство действий (Actions)
Каждое действие - это модуль, экспортирующий функцию `perform`. Эта функция является стандартным обработчиком Express.js и принимает те же параметры:
```ts
import { Request, Response } from "express";
export const perform = (req: Request, res: Response) => {
// req - объект запроса Express.js
// - req.params - параметры маршрута (/users/:id)
// - req.query - параметры запроса (?name=value)
// - req.body - тело запроса (для POST/PUT/PATCH)
// - req.headers - заголовки запроса
// res - объект ответа Express.js
// - res.json() - отправить JSON
// - res.send() - отправить ответ
// - res.render() - отрендерить шаблон
// - res.status() - установить статус
// Пример простого действия
res.json({ message: "Hello!" });
};
```
Примеры типичных действий:
Получение списка ресурсов:
```ts
// actions/posts/indexAction.ts
export const perform = async (req: Request, res: Response) => {
const posts = await Post.findAll();
res.json(posts);
};
```
Просмотр отдельного ресурса:
```ts
// actions/posts/showAction.ts
export const perform = async (req: Request, res: Response) => {
const { id } = req.params;
const post = await Post.findById(id);
if (!post) {
return res.status(404).json({ error: "Post not found" });
}
res.json(post);
};
```
Создание нового ресурса:
```ts
// actions/posts/createAction.ts
export const perform = async (req: Request, res: Response) => {
const { title, content } = req.body;
try {
const post = await Post.create({ title, content });
res.status(201).json(post);
} catch (error) {
res.status(400).json({ error: error.message });
}
};
```
### Middleware
Middleware (промежуточное ПО) - это функции, которые выполняются до обработки запроса действием. Они позволяют:
- Проверять аутентификацию и авторизацию
- Логировать запросы
- Обрабатывать ошибки
- Модифицировать объекты запроса/ответа
- Прерывать цепочку обработки запроса
Middleware можно применять:
- К отдельным маршрутам
- К группам маршрутов через `scope`
- Ко всем маршрутам ресурса
```ts
// Middleware для аутентификации
const authenticate = (req: Request, res: Response, next: Function) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: "No token provided" });
}
try {
req.user = verifyToken(token);
next(); // Продолжить обработку запроса
} catch (error) {
res.status(401).json({ error: "Invalid token" });
}
};
// Применение к отдельному маршруту
get("/profile", [authenticate], "users/profile");
// Применение к группе маршрутов
scope("admin", [authenticate], () => {
resources("users");
});
// Применение ко всем маршрутам ресурса
resources("posts", [authenticate]);
```
### Регулярные выражения
Роутер поддерживает все возможности работы с регулярными выражениями, доступные в оригинальном роутере Express.js. Это позволяет создавать гибкие маршруты для сложных случаев:
:warning: **Внимание!** Данная возможность поддерживается только из-за того, что используется оригинальный роутер `Express.js`. Автор данного роутера не рекомендует использовать регулярные выражения для маршрутов из-за сложности их понимания и поддержки. В случае, если вам действительно нужно использовать регулярные выражения, рекомендуется тщательно тестировать их работу.
```ts
// Маршруты с регулярными выражениями
get(/.*fly$/, "insects#list"); // Совпадёт с /butterfly, /dragonfly
get(/^\/api\/v\d+\/.*$/, "api#handle"); // Совпадёт с /api/v1/users, /api/v2/posts
// Комбинация с middleware
get(/^\/secure\/.*$/, [authenticate], "secure#handle");
// Порядок важен и для регулярных выражений
get(/^\/api\/v1\/users$/, "users#list"); // Сначала конкретный маршрут
get(/^\/api\/v1\/.*$/, "api#handle"); // Затем общий
```
Использование регулярных выражений может быть полезно для:
- Версионирования API
- Обработки групп похожих URL
- Создания гибких правил маршрутизации
- Перехвата специфических паттернов URL
## Тестирование
Действия легко тестировать, так как каждое действие - это отдельный модуль с единственной функцией. Для тестирования используется Jest и SuperTest:
```ts
import request from "supertest";
import express from "express";
import { getRouter, setActionsPath } from "@the-teacher/the-router";
import path from "path";
describe("Posts actions", () => {
let app;
beforeEach(() => {
// Создаём новое приложение для каждого теста
app = express();
// Указываем путь к тестовым действиям
setActionsPath(path.join(__dirname, "./test_actions"));
// Подключаем роутер
app.use(getRouter());
});
describe("GET /posts/:id", () => {
test("returns post by id", async () => {
// Выполняем GET-запрос к /posts/123
const response = await request(app).get("/posts/123").expect(200);
// Проверяем ответ
expect(response.body).toEqual({
action: "show",
id: "123",
});
});
});
describe("POST /posts", () => {
test("creates new post", async () => {
const postData = {
title: "New Post",
content: "Content",
};
// Выполняем POST-запрос с данными
const response = await request(app)
.post("/posts")
.send(postData)
.expect(200);
// Проверяем ответ
expect(response.body).toEqual({
action: "create",
data: postData,
});
});
});
});
```
Тестовые действия могут быть простыми заглушками:
```ts
// test_actions/posts/showAction.ts
import { Request, Response } from "express";
export const perform = (req: Request, res: Response) => {
const { id } = req.params;
res.json({ action: "show", id });
};
// test_actions/posts/createAction.ts
import { Request, Response } from "express";
export const perform = (req: Request, res: Response) => {
res.json({ action: "create", data: req.body });
};
```
Для тестирования middleware:
```ts
test("requires authentication", async () => {
const authenticate = (req: any, res: any, next: any) => {
const auth = req.headers.authorization;
if (auth === "Bearer valid-token") {
next();
} else {
res.status(401).json({ error: "Unauthorized" });
}
};
// Добавляем middleware к маршруту
get("/profile", [authenticate], "users/profile");
// Тест без токена
await request(app).get("/profile").expect(401);
// Тест с правильным токеном
await request(app)
.get("/profile")
.set("Authorization", "Bearer valid-token")
.expect(200);
});
```