По словам знакомых бэкенд-разработчиков, проще всего в моем случае начать писать серверное приложение на NodeJS с использованием TypeScript.

Любопытно то, что платформа NodeJS, предназначенная только для работы с JavaScript, не выполняет код TypeScript. В связи с этим требуется преобразовать код TypeScript в JavaScript. Такой процесс называется транспиляцией.

Что касается IDE, я поэкспериментировал с 2 инструментами: Visual Studio Code и WebStorm. В итоге отдал предпочтение второму варианту. Как оказалось, мне намного проще работать с WebStorm, поскольку я привык к Android Studio. Оба эти инструмента входят в число продуктов JetBrains, поэтому обладают схожими принципами разработки.

Структура проекта

Существует множество способов организации файлов в серверном приложении. Рассмотрим один из них, более соответствующий проекту Android:

Структура проекта 

Прокомментируем эту схему, проводя терминологические параллели с разработкой Android.

  • package.json  —  это своего рода синтез build.gradle и AndroidManifest.xml, если посмотреть на проект с точки зрения разработчика Android. В этом файле мы определяем: имя и версию приложения; главный файл, в котором оно запускается; скрипты, например задачи Gradle; зависимости; зависимости разработки и другое.
  • __test__  —  каталог для размещения модульных тестов. На стороне сервера в качестве фреймворка используется Jest, аналог JUnit в разработке Android.
  • jest.config.js  —  файл, в котором мы определяем конфигурацию тестов и способ их выполнения.  
  • src  —  каталог для группировки кода, что-то наподобие главного модуля приложения main.

Архитектура 

Архитектура

Точки входа 

На стороне сервера в качестве точек входа используются не Activity и Fragments, а маршруты (англ. routes). У нас нет UI, которого может коснуться пользователь. Приложения, которые в данном случае являются пользователями, вызывают разные маршруты: GET, POST, PUT и другие. Рассмотрим пример маршрута.

Пример 1. Базовая конфигурация для применения Express с пользовательским объектом Router.

import express, { Request, Response, NextFunction } from "express";
require('express-async-errors');
import { historyRoutes } from "./routes/history.routes";

const app = express();

app.use(express.json());
app.use("/history", historyRoutes);

Express  —  это фреймворк, упрощающий процесс создания маршрутов. Как видно, файл app.ts импортирует historyRoutes, уже определенный файлом historic.routes.ts. Рассмотрим пример метода POST, делегированного определенному контроллеру, который перенаправляет запрос в конкретный UseCase

Пример 2. Объект Controller обрабатывает запрос POST.

import { Router } from "express";
import { CreateNewHistoricEntryController } from "../../controllers/CreateNewHistoricEntryController";

const historyRoutes = Router();

const createNewHistoricEntryController = new CreateNewHistoricEntryController();
historyRoutes.post("/historic", createNewHistoricEntryController.handle);

export { historyRoutes };

Внедрение зависимостей

Если вы с удовольствием работаете с Koin и Kodein в мире Android, то с легкостью освоите TSyringe в качестве библиотеки для внедрения зависимостей.

Следующий код отображает файл server.ts, в котором мы регистрируем ссылку на Singleton для FirestoreDataSource, реализующего интерфейс ISisOrgRepository. Этот файл подобен классу Application и является точкой входа приложения.

Пример 3. Добавление экземпляра репозитория Application, который будет доступен для всего приложения.

import { container } from "tsyringe";
...
function setup() {
container.registerSingleton<ISisOrgRepository>(
"ISisOrgRepository",
FirestoreDataSource
);
}
...
app.listen(port, () => {
setup();
console.log("Server is running...");
});

Библиотека TSyringe знает, что если кому-то потребуется ссылка на ISisOrgRepository, она обязана предоставить экземпляр FirestoreDataSource. В этом случае класс CreateNewHistoricEntryController должен создать CreateNewHistoricEntryUseCase. Но для этого необходимо попросить TSyringe разрешить зависимости (аргумент конструктора ISisOrgRepository).

Пример 4. UseCase с аннотациями, помогающими библиотеке для внедрения зависимостей.

@injectable()
class CreateNewHistoricEntryUseCase {
constructor(
@inject("ISisOrgRepository")
private sisOrgRepository: ISisOrgRepository
) {
}

async execute(request: IRequest): Promise<IHistoricEntry[]> {
const historicEntries: IHistoricEntry[] = [];
...
await this.sisOrgRepository.createHistoricEntry(historicEntries);
return historicEntries;
}
}

export { CreateNewHistoricEntryUseCase };

Внутри CreateNewHistoricEntryController мы можем попросить библиотеку для внедрения зависимостей о содействии в создании объекта CreateNewHistoricEntryUseCase.

Пример 5. Контроллер запрашивает ссылку на контейнер UseCase.

import { Request, Response } from "express";
import { container } from "tsyringe";
import { CreateNewHistoricEntryUseCase } from "../domain/useCases/CreateNewHistoricEntryUseCase";

class CreateNewHistoricEntryController {
async handle(request: Request, response: Response): Promise<Response> {
const { beds, activity, crop, variety, resultIndicator, input } = request.body;

const useCase = container.resolve(CreateNewHistoricEntryUseCase);
const result = await useCase.execute({
beds, activity, crop, variety, resultIndicator: resultIndicator, input
});
return response.status(201).json(result);
}
}

export { CreateNewHistoricEntryController };

Тесты 

Я опробовал две очень похожие библиотеки: Jasmine и Jest. В итоге выбрал Jest, поскольку она предоставляет отличный формат вывода результатов. 

Пример 6. Отчет о результатах выполнения от Jest.

Синтаксис сильно отличается от тестов JUnit, использующих Kotlin. Однако все проясняется, если понимать describe как имя набора тестов, а it  —  как имя модульного теста. Часть, касающаяся утверждений, не представляет сложности и напоминает принцип работы с библиотекой Truth

Пример 7. Тест с применением Jest в TypeScript  —  класс TimeHelper.

mport { TimeHelper } from "util/TimeHelper"

describe("Time Helper", () => {
it("should get total minutes from 3h", () => {
const totalMinutes = TimeHelper.getTotalMinutes("3h")
expect(totalMinutes).toBe(180)
})

it("should get total minutes from 3h 15min", () => {
const totalMinutes = TimeHelper.getTotalMinutes("3h 15min")
expect(totalMinutes).toBe(195)
})
})

Дополнительные рекомендации 

  • Heroku  —  отличная платформа для размещения API. Она предоставляет удобный CLI. Если package.json в порядке, то при каждой отправке кода CLI автоматически выполняет скрипты (транспиляцию, установку зависимостей, запуск сервера). 
  • Insomnia  —  превосходный инструмент для тестирования маршрутов. С его помощью можно создавать различные среды (разработки, продакшн и т.д.) 
  • Swagger позволяет создавать содержательную документацию, в которой можно запускать маршруты (пример по ссылке). 

Заключение 

Если вы начинаете задействовать новые технологии, советую обращаться к опыту специалистов и обсуждать с ними технические вопросы. Их оперативные рекомендации помогут сэкономить время и быстрее научиться всему необходимому. 

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Gabriel Bronzatti Moro: An API Project From an Android Developer’s Perspective

Предыдущая статьяКак написать тест-раннер в 80 строк кода на JavaScript/TypeScript
Следующая статьяРешение крупномасштабных задач машинного обучения на Python