Підвищте типову безпеку ваших Express.js додатків за допомогою TypeScript. Посібник охоплює обробники маршрутів, типізацію проміжного ПЗ та найкращі практики для масштабованих API.
Інтеграція TypeScript та Express: Типова безпека обробників маршрутів
TypeScript став наріжним каменем сучасної розробки JavaScript, пропонуючи можливості статичної типізації, які підвищують якість, підтримуваність та масштабованість коду. У поєднанні з Express.js, популярним фреймворком веб-додатків для Node.js, TypeScript може значно покращити надійність ваших бекенд-API. Цей вичерпний посібник досліджує, як використовувати TypeScript для досягнення типової безпеки обробників маршрутів у програмах Express.js, надаючи практичні приклади та найкращі практики для створення надійних і підтримуваних API для глобальної аудиторії.
Чому типова безпека важлива в Express.js
У динамічних мовах, таких як JavaScript, помилки часто виявляються під час виконання, що може призвести до несподіваної поведінки та проблем, які важко налагоджувати. TypeScript вирішує цю проблему, вводячи статичну типізацію, що дозволяє виявляти помилки під час розробки, перш ніж вони потраплять у продакшн. У контексті Express.js типова безпека є особливо важливою для обробників маршрутів, де ви маєте справу з об'єктами запитів і відповідей, параметрами запитів та тілами запитів. Неправильна обробка цих елементів може призвести до збоїв програми, пошкодження даних та вразливостей безпеки.
- Раннє виявлення помилок: Виявляйте помилки, пов'язані з типами, під час розробки, зменшуючи ймовірність несподіванок під час виконання.
- Покращена підтримуваність коду: Анотації типів полегшують розуміння та рефакторинг коду.
- Покращене автозавершення коду та інструментарій: IDE можуть надавати кращі пропозиції та перевірку помилок з інформацією про типи.
- Зменшення кількості багів: Типова безпека допомагає запобігти поширеним програмним помилкам, таким як передача неправильних типів даних до функцій.
Налаштування проекту TypeScript Express.js
Перш ніж зануритися в типову безпеку обробників маршрутів, налаштуємо базовий проект TypeScript Express.js. Це послугуватиме основою для наших прикладів.
Передумови
- Встановлені Node.js та npm (Node Package Manager). Ви можете завантажити їх з офіційного веб-сайту Node.js. Переконайтеся, що у вас остання версія для оптимальної сумісності.
- Редактор коду, такий як Visual Studio Code, який пропонує чудову підтримку TypeScript.
Ініціалізація проекту
- Створіть нову директорію проекту:
mkdir typescript-express-app && cd typescript-express-app - Ініціалізуйте новий проект npm:
npm init -y - Встановіть TypeScript та Express.js:
npm install typescript express - Встановіть файли оголошень TypeScript для Express.js (важливо для типової безпеки):
npm install @types/express @types/node - Ініціалізуйте TypeScript:
npx tsc --init(Це створює файлtsconfig.json, який конфігурує компілятор TypeScript.)
Налаштування TypeScript
Відкрийте файл tsconfig.json і налаштуйте його відповідним чином. Ось приклад конфігурації:
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Ключові конфігурації, на які варто звернути увагу:
target: Задає цільову версію ECMAScript.es6є хорошою відправною точкою.module: Задає генерацію коду модуля.commonjs— поширений вибір для Node.js.outDir: Задає вихідну директорію для скомпільованих файлів JavaScript.rootDir: Задає кореневу директорію ваших вихідних файлів TypeScript.strict: Вмикає всі опції суворої перевірки типів для посилення типової безпеки. Це дуже рекомендовано.esModuleInterop: Вмикає сумісність між CommonJS та ES Modules.
Створення точки входу
Створіть директорію src та додайте файл index.ts:
mkdir src
touch src/index.ts
Заповніть файл src/index.ts базовим налаштуванням сервера Express.js:
import express, { Request, Response } from 'express';
const app = express();
const port = 3000;
app.get('/', (req: Request, res: Response) => {
res.send('Hello, TypeScript Express!');
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
Додавання скрипта збирання
Додайте скрипт збирання до файлу package.json для компіляції коду TypeScript:
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "npm run build && npm run start"
}
Тепер ви можете запустити npm run dev, щоб зібрати та запустити сервер.
Типова безпека обробників маршрутів: Визначення типів запитів та відповідей
Суть типової безпеки обробників маршрутів полягає у правильному визначенні типів для об'єктів Request та Response. Express.js надає узагальнені типи для цих об'єктів, які дозволяють вказувати типи параметрів запиту, тіла запиту та параметрів маршруту.
Базові типи обробників маршрутів
Почнемо з простого обробника маршруту, який очікує ім'я як параметр запиту:
import express, { Request, Response } from 'express';
const app = express();
const port = 3000;
interface NameQuery {
name: string;
}
app.get('/hello', (req: Request, res: Response) => {
const name = req.query.name;
if (!name) {
return res.status(400).send('Name parameter is required.');
}
res.send(`Hello, ${name}!`);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
У цьому прикладі:
Request<any, any, any, NameQuery>визначає тип для об'єкта запиту.- Перший
anyпредставляє параметри маршруту (наприклад,/users/:id). - Другий
anyпредставляє тип тіла відповіді. - Третій
anyпредставляє тип тіла запиту. NameQuery– це інтерфейс, який визначає структуру параметрів запиту.
Визначивши інтерфейс NameQuery, TypeScript тепер може перевірити, чи існує властивість req.query.name і чи має вона тип string. Якщо ви спробуєте отримати доступ до неіснуючої властивості або призначити значення неправильного типу, TypeScript позначить помилку.
Обробка тіл запитів
Для маршрутів, які приймають тіла запитів (наприклад, POST, PUT, PATCH), ви можете визначити інтерфейс для тіла запиту та використовувати його в типі Request:
import express, { Request, Response } from 'express';
import bodyParser from 'body-parser';
const app = express();
const port = 3000;
app.use(bodyParser.json()); // Важливо для парсингу тіл JSON-запитів
interface CreateUserRequest {
firstName: string;
lastName: string;
email: string;
}
app.post('/users', (req: Request, res: Response) => {
const { firstName, lastName, email } = req.body;
// Перевірка тіла запиту
if (!firstName || !lastName || !email) {
return res.status(400).send('Missing required fields.');
}
// Обробка створення користувача (наприклад, збереження в базу даних)
console.log(`Creating user: ${firstName} ${lastName} (${email})`);
res.status(201).send('User created successfully.');
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
У цьому прикладі:
CreateUserRequestвизначає структуру очікуваного тіла запиту.app.use(bodyParser.json())є критично важливим для парсингу тіл JSON-запитів. Без ньогоreq.bodyбуде невизначеним.- Тип
RequestтеперRequest<any, any, CreateUserRequest>, що вказує, що тіло запиту має відповідати інтерфейсуCreateUserRequest.
TypeScript тепер забезпечить, щоб об'єкт req.body містив очікувані властивості (firstName, lastName та email) і щоб їхні типи були правильними. Це значно зменшує ризик помилок під час виконання, спричинених некоректними даними тіла запиту.
Обробка параметрів маршруту
Для маршрутів з параметрами (наприклад, /users/:id) ви можете визначити інтерфейс для параметрів маршруту та використовувати його в типі Request:
import express, { Request, Response } from 'express';
const app = express();
const port = 3000;
interface UserParams {
id: string;
}
interface User {
id: string;
firstName: string;
lastName: string;
email: string;
}
const users: User[] = [
{ id: '1', firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com' },
{ id: '2', firstName: 'Jane', lastName: 'Smith', email: 'jane.smith@example.com' },
];
app.get('/users/:id', (req: Request, res: Response) => {
const userId = req.params.id;
const user = users.find(u => u.id === userId);
if (!user) {
return res.status(404).send('User not found.');
}
res.json(user);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
У цьому прикладі:
UserParamsвизначає структуру параметрів маршруту, вказуючи, що параметрidмає бути рядком.- Тип
RequestтеперRequest<UserParams>, що вказує, що об'єктreq.paramsмає відповідати інтерфейсуUserParams.
TypeScript тепер забезпечить, що властивість req.params.id існує і має тип string. Це допомагає запобігти помилкам, викликаним доступом до неіснуючих параметрів маршруту або використанням їх з неправильними типами.
Зазначення типів відповідей
Хоча зосередження на типовій безпеці запитів є критично важливим, визначення типів відповідей також покращує чіткість коду та допомагає запобігти неузгодженостям. Ви можете визначити тип даних, які ви надсилаєте у відповідь.
import express, { Request, Response } from 'express';
const app = express();
const port = 3000;
interface User {
id: string;
firstName: string;
lastName: string;
email: string;
}
const users: User[] = [
{ id: '1', firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com' },
{ id: '2', firstName: 'Jane', lastName: 'Smith', email: 'jane.smith@example.com' },
];
app.get('/users', (req: Request, res: Response) => {
res.json(users);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
Тут Response<User[]> вказує, що тіло відповіді має бути масивом об'єктів User. Це допомагає забезпечити, що ви послідовно надсилаєте правильну структуру даних у відповідях вашого API. Якщо ви спробуєте надіслати дані, які не відповідають типу `User[]`, TypeScript видасть попередження.
Типова безпека проміжного програмного забезпечення
Функції проміжного програмного забезпечення є важливими для обробки наскрізних функцій у програмах Express.js. Забезпечення типової безпеки в проміжному програмному забезпеченні є таким же важливим, як і в обробниках маршрутів.
Типізація функцій проміжного програмного забезпечення
Базова структура функції проміжного програмного забезпечення в TypeScript схожа на структуру обробника маршруту:
import express, { Request, Response, NextFunction } from 'express';
function authenticationMiddleware(req: Request, res: Response, next: NextFunction) {
// Логіка автентифікації
const isAuthenticated = true; // Замініть на фактичну перевірку автентифікації
if (isAuthenticated) {
next(); // Перейти до наступного проміжного програмного забезпечення або обробника маршруту
} else {
res.status(401).send('Unauthorized');
}
}
const app = express();
const port = 3000;
app.use(authenticationMiddleware);
app.get('/', (req: Request, res: Response) => {
res.send('Hello, authenticated user!');
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
У цьому прикладі:
NextFunction— це тип, наданий Express.js, який представляє наступну функцію проміжного програмного забезпечення в ланцюжку.- Функція проміжного програмного забезпечення приймає ті ж об'єкти
RequestтаResponse, що й обробники маршрутів.
Доповнення об'єкта Request
Іноді ви можете захотіти додати власні властивості до об'єкта Request у своєму проміжному програмному забезпеченні. Наприклад, проміжне програмне забезпечення для автентифікації може додати властивість user до об'єкта запиту. Щоб зробити це типобезпечним способом, вам потрібно доповнити інтерфейс Request.
import express, { Request, Response, NextFunction } from 'express';
interface User {
id: string;
username: string;
email: string;
}
// Доповнення інтерфейсу Request
declare global {
namespace Express {
interface Request {
user?: User;
}
}
}
function authenticationMiddleware(req: Request, res: Response, next: NextFunction) {
// Логіка автентифікації (замініть на фактичну перевірку автентифікації)
const user: User = { id: '123', username: 'johndoe', email: 'john.doe@example.com' };
req.user = user; // Додати користувача до об'єкта запиту
next(); // Перейти до наступного проміжного програмного забезпечення або обробника маршруту
}
const app = express();
const port = 3000;
app.use(authenticationMiddleware);
app.get('/', (req: Request, res: Response) => {
const username = req.user?.username || 'Guest';
res.send(`Hello, ${username}!`);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
У цьому прикладі:
- Ми використовуємо глобальне оголошення для доповнення інтерфейсу
Express.Request. - Ми додаємо необов'язкову властивість
userтипуUserдо інтерфейсуRequest. - Тепер ви можете отримати доступ до властивості
req.userу своїх обробниках маршрутів, не викликаючи скарг TypeScript. Символ `?` у `req.user?.username` є вирішальним для обробки випадків, коли користувач не автентифікований, запобігаючи потенційним помилкам.
Найкращі практики інтеграції TypeScript та Express
Щоб максимально використати переваги TypeScript у ваших програмах Express.js, дотримуйтеся цих найкращих практик:
- Увімкніть строгий режим: Використовуйте опцію
"strict": trueу файліtsconfig.json, щоб увімкнути всі опції суворої перевірки типів. Це допомагає виявляти потенційні помилки на ранніх етапах і забезпечує вищий рівень типової безпеки. - Використовуйте інтерфейси та псевдоніми типів: Визначайте інтерфейси та псевдоніми типів для представлення структури ваших даних. Це робить ваш код більш читабельним і підтримуваним.
- Використовуйте узагальнені типи: Використовуйте узагальнені типи для створення повторно використовуваних і типобезпечних компонентів.
- Пишіть модульні тести: Пишіть модульні тести для перевірки правильності вашого коду та забезпечення точності ваших анотацій типів. Тестування є вирішальним для підтримки якості коду.
- Використовуйте лінтер та форматер: Використовуйте лінтер (наприклад, ESLint) та форматер (наприклад, Prettier) для забезпечення послідовного стилю кодування та виявлення потенційних помилок.
- Уникайте типу
any: Мінімізуйте використання типуany, оскільки він обходить перевірку типів і нівелює мету використання TypeScript. Використовуйте його лише тоді, коли це абсолютно необхідно, і розгляньте можливість використання більш специфічних типів або узагальнень, коли це можливо. - Логічно структуруйте свій проект: Організуйте свій проект за модулями або папками на основі функціональності. Це покращить підтримуваність та масштабованість вашої програми.
- Використовуйте ін'єкцію залежностей: Розгляньте можливість використання контейнера ін'єкції залежностей для керування залежностями вашої програми. Це може зробити ваш код більш тестованим і підтримуваним. Бібліотеки, такі як InversifyJS, є популярним вибором.
Розширені концепції TypeScript для Express.js
Використання декораторів
Декоратори надають стислий та виразний спосіб додавання метаданих до класів та функцій. Ви можете використовувати декоратори для спрощення реєстрації маршрутів в Express.js.
По-перше, вам потрібно увімкнути експериментальні декоратори у файлі tsconfig.json, додавши "experimentalDecorators": true до compilerOptions.
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true
}
}
Потім ви можете створити власний декоратор для реєстрації маршрутів:
import express, { Router, Request, Response } from 'express';
function route(method: string, path: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
if (!target.__router__) {
target.__router__ = Router();
}
target.__router__[method](path, descriptor.value);
};
}
class UserController {
@route('get', '/users')
getUsers(req: Request, res: Response) {
res.send('List of users');
}
@route('post', '/users')
createUser(req: Request, res: Response) {
res.status(201).send('User created');
}
public getRouter() {
return this.__router__;
}
}
const userController = new UserController();
const app = express();
const port = 3000;
app.use('/', userController.getRouter());
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
У цьому прикладі:
- Декоратор
routeприймає метод HTTP та шлях як аргументи. - Він реєструє декорований метод як обробник маршруту на роутері, пов'язаному з класом.
- Це спрощує реєстрацію маршрутів і робить ваш код більш читабельним.
Використання власних охоронців типів
Охоронці типів — це функції, які звужують тип змінної в межах певної області видимості. Ви можете використовувати власні охоронці типів для перевірки тіл запитів або параметрів запиту.
interface Product {
id: string;
name: string;
price: number;
}
function isProduct(obj: any): obj is Product {
return typeof obj === 'object' &&
obj !== null &&
typeof obj.id === 'string' &&
typeof obj.name === 'string' &&
typeof obj.price === 'number';
}
import express, { Request, Response } from 'express';
import bodyParser from 'body-parser';
const app = express();
const port = 3000;
app.use(bodyParser.json());
app.post('/products', (req: Request, res: Response) => {
if (!isProduct(req.body)) {
return res.status(400).send('Invalid product data');
}
const product: Product = req.body;
console.log(`Creating product: ${product.name}`);
res.status(201).send('Product created');
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
У цьому прикладі:
- Функція
isProduct— це власний охоронець типу, який перевіряє, чи відповідає об'єкт інтерфейсуProduct. - Усередині обробника маршруту
/productsфункціяisProductвикористовується для перевірки тіла запиту. - Якщо тіло запиту є дійсним продуктом, TypeScript знає, що
req.bodyмає типProductу блоціif.
Врахування глобальних аспектів у дизайні API
При розробці API для глобальної аудиторії слід враховувати кілька факторів для забезпечення доступності, зручності використання та культурної чутливості.
- Локалізація та інтернаціоналізація (i18n та L10n):
- Узгодження вмісту: Підтримуйте кілька мов та регіонів через узгодження вмісту на основі заголовка
Accept-Language. - Форматування дати та часу: Використовуйте формат ISO 8601 для представлення дати та часу, щоб уникнути неоднозначності в різних регіонах.
- Форматування чисел: Обробляйте форматування чисел відповідно до локалі користувача (наприклад, десяткові та тисячні роздільники).
- Обробка валют: Підтримуйте кілька валют та надавайте інформацію про обмінний курс за необхідності.
- Напрямок тексту: Підтримуйте мови з написанням справа наліво (RTL), такі як арабська та іврит.
- Узгодження вмісту: Підтримуйте кілька мов та регіонів через узгодження вмісту на основі заголовка
- Часові пояси:
- Зберігайте дати та час у UTC (Всесвітній координований час) на стороні сервера.
- Дозвольте користувачам вказувати бажаний часовий пояс та відповідно конвертувати дати та час на стороні клієнта.
- Використовуйте бібліотеки, такі як
moment-timezone, для обробки конвертації часових поясів.
- Кодування символів:
- Використовуйте кодування UTF-8 для всіх текстових даних для підтримки широкого спектру символів з різних мов.
- Переконайтеся, що ваша база даних та інші системи зберігання даних налаштовані на використання UTF-8.
- Доступність:
- Дотримуйтесь настанов щодо доступності (наприклад, WCAG), щоб зробити ваш API доступним для користувачів з обмеженими можливостями.
- Надавайте чіткі та описові повідомлення про помилки, які легко зрозуміти.
- Використовуйте семантичні елементи HTML та атрибути ARIA у документації вашого API.
- Культурна чутливість:
- Уникайте використання культурно специфічних посилань, ідіом або гумору, які можуть бути незрозумілими для всіх користувачів.
- Будьте уважними до культурних відмінностей у стилях спілкування та вподобаннях.
- Враховуйте потенційний вплив вашого API на різні культурні групи та уникайте поширення стереотипів або упереджень.
- Конфіденційність та безпека даних:
- Дотримуйтесь правил конфіденційності даних, таких як GDPR (Загальний регламент захисту даних) та CCPA (Закон штату Каліфорнія про конфіденційність споживачів).
- Запровадьте надійні механізми автентифікації та авторизації для захисту даних користувачів.
- Шифруйте конфіденційні дані як під час передачі, так і в стані спокою.
- Надайте користувачам контроль над їхніми даними та дозвольте їм отримувати доступ, змінювати та видаляти свої дані.
- Документація API:
- Надайте вичерпну та добре організовану документацію API, яка легко зрозуміла та зручна для навігації.
- Використовуйте інструменти, такі як Swagger/OpenAPI, для генерації інтерактивної документації API.
- Включіть приклади коду кількома мовами програмування, щоб задовольнити різноманітну аудиторію.
- Перекладіть документацію вашого API кількома мовами, щоб охопити ширшу аудиторію.
- Обробка помилок:
- Надавайте конкретні та інформативні повідомлення про помилки. Уникайте загальних повідомлень про помилки, таких як "Щось пішло не так".
- Використовуйте стандартні коди стану HTTP для позначення типу помилки (наприклад, 400 для поганого запиту, 401 для неавторизованого доступу, 500 для внутрішньої помилки сервера).
- Включайте коди помилок або ідентифікатори, які можна використовувати для відстеження та налагодження проблем.
- Ведіть журнал помилок на стороні сервера для налагодження та моніторингу.
- Обмеження швидкості (Rate Limiting): Впровадьте обмеження швидкості для захисту вашого API від зловживань та забезпечення справедливого використання.
- Версіонування: Використовуйте версіонування API, щоб дозволити зворотньо сумісні зміни та уникнути порушення роботи існуючих клієнтів.
Висновок
Інтеграція TypeScript та Express значно підвищує надійність та підтримуваність ваших бекенд-API. Використовуючи типову безпеку в обробниках маршрутів та проміжному програмному забезпеченні, ви можете виявляти помилки на ранніх етапах розробки та створювати більш надійні та масштабовані програми для глобальної аудиторії. Визначаючи типи запитів та відповідей, ви гарантуєте, що ваш API дотримується послідовної структури даних, зменшуючи ймовірність помилок під час виконання. Пам'ятайте про дотримання найкращих практик, таких як увімкнення строгого режиму, використання інтерфейсів та псевдоніків типів, а також написання модульних тестів для максимального використання переваг TypeScript. Завжди враховуйте глобальні фактори, такі як локалізація, часові пояси та культурна чутливість, щоб забезпечити доступність та зручність використання ваших API по всьому світу.