Сегодня я расскажу о том, как мы можем с помощью типов написать простое расширение для ExpressJS.
А если вы в своём приложении/приложениях используете только решения на TypeScript(JavaScript), то у вас отпадёт необходимость в Swagger.
Вообще, одно из главных преимуществ разработки серверного кода на NodeJS — это один язык программирования с Web-интерфейсом/React/Vue Native. Это даёт возможность написать общий код в одном месте только один раз и использовать его затем везде.
Именно это мы сейчас с вами и попытаемся сделать.
Представим простой монорепозиторий, который состоит из двух проектов:
npm init
npm install typescript --save-dev
Для имени этого проекта я буду использовать имя монорепозитория и название папки:
// shared/package.json
{
name: @express-ts-react/shared,
...
}
// shared/common/package.json
{
"sideEffects": false,
"main": "./index.js"
}
Там же создадим файл, в котором мы объявим самые важные генерики, которых уже будет достаточно, чтобы заменить нам swagger. EndpointMeta<T,U> — это тип, который описывает один endpoint. По сути, любой метод нашего RestApi задаётся таким набором. T и U здесь использованы не просто так, они понадобятся для автоматического и динамического формирования интерфейсов наших контроллеров. Пока вам нужно знать, что T —это параметры аргументов, которые принимает метод контроллера, а U — это формат ответа.
// shared/common/index.tsx
/**
* REST description about endpoint
* Can be extended with additional fields or methods. For instance, auth protected endpoint
*/
export type EndpointMeta<T = {}, U = {}> = {
/**
* helper for type in runtime definition
*/
_: "endpointMeta";
/**
* Endpoint route
*/
route: `/${string}` | `*`;
/**
* Url in express format without route prefix
*/
url: `/${string}` | `*`;
/**
* Method in express format, can be extended by others
*/
method?: "get" | "post" | "put" | "delete";
};
/**
* Get argument types of endpoint method
*/
export type GetInnerArgsOfMeta<S> = S extends EndpointMeta<infer T, infer S>
? T
: never;
/**
* Get Response type of endpoint method
*/
export type GetInnerResponseOfMeta<S> = S extends EndpointMeta<infer T, infer S>
? S
: never;
/**
* Get endpoint type, where key is the endpoint name,
* args - is the endpoint method arguments
* and result is endpoint response
*/
export type EndpointsProvider<T extends typeof endpoints> = {
[key in keyof T]: (
args: GetInnerArgsOfMeta<T[key]>
) => GetInnerResponseOfMeta<T[key]>;
};
А теперь используя только эти четыре типа вы можете легко создавать интерфейсы/контракты для ваших контроллеров, и использовать этот код как на сервере, так и на клиенте. Снизу, например, представлены методы для нашего ToDo приложения:
// shared/server/index.tsx
import { EndpointMeta, EndpointsProvider } from "../shared";
const getTasks: EndpointMeta<
{
query?: {
status?: boolean;
ids?: string[];
};
},
Promise<Task[]>
> = {
_: "endpointMeta",
url: "/",
method: "get",
};
const getTaskById: EndpointMeta<
{
params: {
id: string;
};
},
Promise<Task>
> = {
_: "endpointMeta",
url: "/:id",
method: "get",
};
const addTask: EndpointMeta<{ body: Task }, Promise<Task>> = {
_: "endpointMeta",
url: "/:id",
method: "post",
};
const deleteTaskById: EndpointMeta<
{
params: {
id: string;
};
},
Promise<Task>
> = {
_: "endpointMeta",
url: "/:id",
method: "delete",
};
// Our final endpoints collection
const endpoints = {
getTasks,
getTaskById,
addTask,
deleteTaskById,
};
Четыре переменные, по сути, содержат всю необходимую информацию, которую нужно, чтобы:
// shard/server/index.tsx
class TaskController implements EndpointsProvider<typeof endpoints> {}
Если мы оставим класс как есть, то, во-первых, на нас будет ругаться vscode, а во-вторых, при попытки собрать проект командой npx tsc мы увидим ошибку:
blog/index.ts:77:7 - error TS2420: Class 'TaskController' incorrectly implements interface 'EndpointsProvider<{ getTasks: EndpointMeta<{ query?: { status?: boolean | undefined; ids?: string[] | undefined; } | undefined; }, Task[]>; getTaskById: EndpointMeta<{ params: { id: string; }; }, Task>; addTask: EndpointMeta<...>; deleteTaskById: EndpointMeta<...>; }>'.
Type 'TaskController' is missing the following properties from type 'EndpointsProvider<{ getTasks: EndpointMeta<{ query?: { status?: boolean | undefined; ids?: string[] | undefined; } | undefined; }, Task[]>; getTaskById: EndpointMeta<{ params: { id: string; }; }, Task>; addTask: EndpointMeta<...>; deleteTaskById: EndpointMeta<...>; }>': getTasks, getTaskById, addTask, deleteTaskById
77 class TaskController implements EndpointsProvider<typeof endpoints> {}
Found 1 error.
Эта ошибка говорит, что наш класс TaskController неправильно реализует EndpointsProvider: отсутствуют функции addTask, getTaskById, getTasks, deleteTask. Давайте теперь попробуем реализовать эти методы.
// shared/server/index.tsx
...
class TaskController implements EndpointsProvider<typeof endpoints> {
getTaskById: (args: {
params: {
id: string;
};
}) => Task = (args) => {
return {
id: "",
description: "",
done: true,
};
};
getTasks: (args: {
query?: {
status?: boolean;
ids?: string[];
};
}) => Task[] = (args) => {
return [{
id: "",
description: "",
done: true,
}];
};
addTask: (args: { body: Task }) => Task = (args) => {
return args.body;
};
deleteTaskById: (args: {
params: {
id: string;
};
}) => Task = (args) => {
return {
id: "",
description: "",
done: true,
};
};
}
Теперь команда npx tsx ничего не пишет нам, а значит, всё работает! Как видно, это класс для контроллера, и в нём уже содержится информация о конкретной реализации методов, поэтому по-хорошему этот класс нужно уже выносить в проект нашего сервера. Чтобы сделать клиент, нужно унаследовать точно такой же интерфейс, но уже вместо конкретной реализации, нужно заменять url на параметры и формировать fetch запрос на сервер.
В следующей части я покажу, как можно автоматизировать на основе const endpoints формирование сервера и класса клиента. А также мы создадим базовый контроллер, запрос и ответ, сделаем их расширяемыми собственными свойствами и спасём себе жизнь от ошибок и сохраним часы и дни для синхронизации работы серверов и клиентов.
При использовании TypeScript код заметно увеличивается в размере, и кому-то это может казаться некрасивым. Но когда вы работаете в современной IDE или текстовом редакторе на стероидах (VSCode) - то вы оцените, насколько силён TypeScript. Его использование существенно снижает количество ошибок, а также доступность для понимания кода для вас и членов вашей команды.
А если вы в своём приложении/приложениях используете только решения на TypeScript(JavaScript), то у вас отпадёт необходимость в Swagger.
Вообще, одно из главных преимуществ разработки серверного кода на NodeJS — это один язык программирования с Web-интерфейсом/React/Vue Native. Это даёт возможность написать общий код в одном месте только один раз и использовать его затем везде.
Именно это мы сейчас с вами и попытаемся сделать.
Представим простой монорепозиторий, который состоит из двух проектов:
- server: Backend WebAPI, написанный на ExpressJS;
- client: Frontend SPA-клиент, написанный на VanillaJS.
Объявляем общие типы
Для начала создадим другую папку в нашем монорепозитории и назовём её shared. Сделаем его npm проектом на typescript, выполнив в корне папке команды:npm init
npm install typescript --save-dev
Для имени этого проекта я буду использовать имя монорепозитория и название папки:
// shared/package.json
{
name: @express-ts-react/shared,
...
}
Теперь создадим папку common в проекте @express-ts-react/shared и объявим её локальным модулем:Несколько замечаний об этом проекте
Поскольку подразумевается, что код в этом проекте будет использоваться в разных службах и приложениях, то необходимо соблюдать следующие правила:
- Не устанавливать специфичные фреймворки и библиотеки;
- Желательно вообще ничего не устанавливать
- Старайтесь писать здесь максимально абстрактный код, или код, который будет работать везде.
- Каждое приложение или сервер, если предоставляет какие-то контракты для работы с ним, должно иметь собственную папку, и оно не должно ниоткуда, кроме common импортировать код.
// shared/common/package.json
{
"sideEffects": false,
"main": "./index.js"
}
Там же создадим файл, в котором мы объявим самые важные генерики, которых уже будет достаточно, чтобы заменить нам swagger. EndpointMeta<T,U> — это тип, который описывает один endpoint. По сути, любой метод нашего RestApi задаётся таким набором. T и U здесь использованы не просто так, они понадобятся для автоматического и динамического формирования интерфейсов наших контроллеров. Пока вам нужно знать, что T —это параметры аргументов, которые принимает метод контроллера, а U — это формат ответа.
// shared/common/index.tsx
/**
* REST description about endpoint
* Can be extended with additional fields or methods. For instance, auth protected endpoint
*/
export type EndpointMeta<T = {}, U = {}> = {
/**
* helper for type in runtime definition
*/
_: "endpointMeta";
/**
* Endpoint route
*/
route: `/${string}` | `*`;
/**
* Url in express format without route prefix
*/
url: `/${string}` | `*`;
/**
* Method in express format, can be extended by others
*/
method?: "get" | "post" | "put" | "delete";
};
/**
* Get argument types of endpoint method
*/
export type GetInnerArgsOfMeta<S> = S extends EndpointMeta<infer T, infer S>
? T
: never;
/**
* Get Response type of endpoint method
*/
export type GetInnerResponseOfMeta<S> = S extends EndpointMeta<infer T, infer S>
? S
: never;
/**
* Get endpoint type, where key is the endpoint name,
* args - is the endpoint method arguments
* and result is endpoint response
*/
export type EndpointsProvider<T extends typeof endpoints> = {
[key in keyof T]: (
args: GetInnerArgsOfMeta<T[key]>
) => GetInnerResponseOfMeta<T[key]>;
};
А теперь используя только эти четыре типа вы можете легко создавать интерфейсы/контракты для ваших контроллеров, и использовать этот код как на сервере, так и на клиенте. Снизу, например, представлены методы для нашего ToDo приложения:
// shared/server/index.tsx
import { EndpointMeta, EndpointsProvider } from "../shared";
const getTasks: EndpointMeta<
{
query?: {
status?: boolean;
ids?: string[];
};
},
Promise<Task[]>
> = {
_: "endpointMeta",
url: "/",
method: "get",
};
const getTaskById: EndpointMeta<
{
params: {
id: string;
};
},
Promise<Task>
> = {
_: "endpointMeta",
url: "/:id",
method: "get",
};
const addTask: EndpointMeta<{ body: Task }, Promise<Task>> = {
_: "endpointMeta",
url: "/:id",
method: "post",
};
const deleteTaskById: EndpointMeta<
{
params: {
id: string;
};
},
Promise<Task>
> = {
_: "endpointMeta",
url: "/:id",
method: "delete",
};
// Our final endpoints collection
const endpoints = {
getTasks,
getTaskById,
addTask,
deleteTaskById,
};
Четыре переменные, по сути, содержат всю необходимую информацию, которую нужно, чтобы:
- Создать ExpressJs роутинг;
- Без подглядывания в сторонние данные написать клиент для этого сервера.
// shard/server/index.tsx
class TaskController implements EndpointsProvider<typeof endpoints> {}
Если мы оставим класс как есть, то, во-первых, на нас будет ругаться vscode, а во-вторых, при попытки собрать проект командой npx tsc мы увидим ошибку:
blog/index.ts:77:7 - error TS2420: Class 'TaskController' incorrectly implements interface 'EndpointsProvider<{ getTasks: EndpointMeta<{ query?: { status?: boolean | undefined; ids?: string[] | undefined; } | undefined; }, Task[]>; getTaskById: EndpointMeta<{ params: { id: string; }; }, Task>; addTask: EndpointMeta<...>; deleteTaskById: EndpointMeta<...>; }>'.
Type 'TaskController' is missing the following properties from type 'EndpointsProvider<{ getTasks: EndpointMeta<{ query?: { status?: boolean | undefined; ids?: string[] | undefined; } | undefined; }, Task[]>; getTaskById: EndpointMeta<{ params: { id: string; }; }, Task>; addTask: EndpointMeta<...>; deleteTaskById: EndpointMeta<...>; }>': getTasks, getTaskById, addTask, deleteTaskById
77 class TaskController implements EndpointsProvider<typeof endpoints> {}
Found 1 error.
Эта ошибка говорит, что наш класс TaskController неправильно реализует EndpointsProvider: отсутствуют функции addTask, getTaskById, getTasks, deleteTask. Давайте теперь попробуем реализовать эти методы.
// shared/server/index.tsx
...
class TaskController implements EndpointsProvider<typeof endpoints> {
getTaskById: (args: {
params: {
id: string;
};
}) => Task = (args) => {
return {
id: "",
description: "",
done: true,
};
};
getTasks: (args: {
query?: {
status?: boolean;
ids?: string[];
};
}) => Task[] = (args) => {
return [{
id: "",
description: "",
done: true,
}];
};
addTask: (args: { body: Task }) => Task = (args) => {
return args.body;
};
deleteTaskById: (args: {
params: {
id: string;
};
}) => Task = (args) => {
return {
id: "",
description: "",
done: true,
};
};
}
Теперь команда npx tsx ничего не пишет нам, а значит, всё работает! Как видно, это класс для контроллера, и в нём уже содержится информация о конкретной реализации методов, поэтому по-хорошему этот класс нужно уже выносить в проект нашего сервера. Чтобы сделать клиент, нужно унаследовать точно такой же интерфейс, но уже вместо конкретной реализации, нужно заменять url на параметры и формировать fetch запрос на сервер.
В следующей части я покажу, как можно автоматизировать на основе const endpoints формирование сервера и класса клиента. А также мы создадим базовый контроллер, запрос и ответ, сделаем их расширяемыми собственными свойствами и спасём себе жизнь от ошибок и сохраним часы и дни для синхронизации работы серверов и клиентов.
При использовании TypeScript код заметно увеличивается в размере, и кому-то это может казаться некрасивым. Но когда вы работаете в современной IDE или текстовом редакторе на стероидах (VSCode) - то вы оцените, насколько силён TypeScript. Его использование существенно снижает количество ошибок, а также доступность для понимания кода для вас и членов вашей команды.
Использование типов TypeScript вместо Swagger
Сегодня я расскажу о том, как мы можем с помощью типов написать простое расширение для ExpressJS....
habr.com