TypeScript и Node.js
Дальше: TypeORM · Асинхронность · Обработка ошибок · Форматы и подключение
Node.js выполняет скомпилированный JavaScript; TypeScript задаёт контракты для HTTP API, файлов, БД и доменной логики. Один язык на frontend и backend упрощает общие типы DTO — при дисциплине слоёв.
Маршрут: Первая программа → Форматы и подключение → Node → TypeORM.
Runtime Node: раздел Node в JS. Монорепо — 3.md.
TypeScript на backend
| Выгода | Пример |
|---|---|
| Контракт API | CreateUserDto vs UserEntity |
| Рефакторинг | переименование поля ловит все handlers |
| CI | tsc --noEmit на каждый PR |
| Общие типы | пакет @myorg/contracts в monorepo |
TS не отменяет валидацию тела запроса: JSON из сети — unknown до проверки — 6.md.
Минимальный проект
mkdir my-api && cd my-api
npm init -y
npm i -D typescript @types/node tsx
npx tsc --init
tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"outDir": "dist",
"rootDir": "src",
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
package.json:
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts",
"typecheck": "tsc --noEmit"
}
}
Разбор:
NodeNext— ESM/CJS по правилам Node 20+ — 9.md.tsx— dev без ручной пересборки; production —tsc+node.
Слои: DTO, domain, persistence
// types/dto.ts — контракт HTTP
export type CreateUserDto = {
email: string;
name: string;
};
// domain/user.ts — бизнес-сущность
export type User = {
id: string;
email: string;
name: string;
createdAt: Date;
};
// mappers
export function fromDto(dto: CreateUserDto): Omit<User, "id" | "createdAt"> {
return { email: dto.email.trim(), name: dto.name.trim() };
}
Разбор:
- DTO отражает тело запроса; domain — то, с чем работает сервис.
- Поля БД (
created_at) не протекают в API без маппера.
Сервис и Result
type ServiceError = "validation" | "conflict";
type ServiceResult<T> =
| { ok: true; value: T }
| { ok: false; error: ServiceError };
export function createUser(
input: CreateUserDto,
existingEmails: Set<string>,
): ServiceResult<User> {
if (!input.email.includes("@")) {
return { ok: false, error: "validation" };
}
if (existingEmails.has(input.email)) {
return { ok: false, error: "conflict" };
}
const user: User = {
id: crypto.randomUUID(),
...fromDto(input),
createdAt: new Date(),
};
return { ok: true, value: user };
}
Подробнее Result — 27.md.
Handler в стиле Express
import type { Request, Response } from "express";
import type { CreateUserDto } from "./types/dto.js";
type ApiOk<T> = { ok: true; data: T };
type ApiErr = { ok: false; error: string };
type ApiResponse<T> = ApiOk<T> | ApiErr;
function isCreateUserDto(body: unknown): body is CreateUserDto {
if (typeof body !== "object" || body === null) return false;
const o = body as Record<string, unknown>;
return typeof o.email === "string" && typeof o.name === "string";
}
export async function postUser(
req: Request,
res: Response<ApiResponse<User>>,
): Promise<void> {
if (!isCreateUserDto(req.body)) {
res.status(400).json({ ok: false, error: "Invalid body" });
return;
}
const result = createUser(req.body, new Set());
if (!result.ok) {
const status = result.error === "conflict" ? 409 : 400;
res.status(status).json({ ok: false, error: result.error });
return;
}
res.status(201).json({ ok: true, data: result.value });
}
Разбор:
body—unknownдо guard.- Ответ типизирован union — клиент на TS может переиспользовать тип.
NestJS, Fastify — те же принципы DTO; декораторы — 23.md.
Файлы: fs/promises
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
async function loadConfig(dir: string): Promise<Record<string, string>> {
const file = path.join(dir, "config.json");
const raw = await readFile(file, "utf8");
const data: unknown = JSON.parse(raw);
if (typeof data !== "object" || data === null) {
throw new Error("Invalid config");
}
return data as Record<string, string>;
}
Разбор:
@types/nodeдаёт типы дляfs,path,crypto.- Async — 17.md.
Структура backend-проекта
src/
├── index.ts # bootstrap
├── app.ts # HTTP app
├── modules/
│ └── users/
│ ├── user.service.ts
│ ├── user.routes.ts
│ └── user.types.ts
└── shared/
├── errors.ts
└── types/
| Слой | Ответственность |
|---|---|
| routes / controllers | HTTP, статусы |
| services | бизнес-логика, Result |
| repositories | БД — 26.md |
Переменные окружения
function requireEnv(name: string): string {
const v = process.env[name];
if (!v) throw new Error(`Missing env: ${name}`);
return v;
}
const port = Number(requireEnv("PORT"));
Для сложных конфигов — Zod-схема env — 6.md.
CI
- run: npm ci
- run: npm run typecheck
- run: npm test
- run: npm run build
Разбор:
typecheckиbuild— разные шаги, если bundler не вызываетtsc— 3.md.
Частые ошибки
| Ошибка | Что делать |
|---|---|
req.body as Dto | type guard |
| Один тип на DTO и ORM entity | разделить + mapper |
CommonJS/import без .js | NodeNext + расширение в import |
Нет @types/node | devDependency |
any в middleware | типизировать Request augmentation |
Практика
- Поднимите API с одним
POST /usersи guard тела. - Разделите
CreateUserDto,User, ответ API. - Добавьте
typecheckв CI. - Прочитайте файл конфига через
fs/promisesсunknown+ проверкой. - Верните
409приconflictизServiceResult.
Смежные статьи
- Подключение · Async · Ошибки · TypeORM
- Экосистема · Компиляция
- 5.md — мини-проекты