По словам знакомых бэкенд-разработчиков, проще всего в моем случае начать писать серверное приложение на 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 позволяет создавать содержательную документацию, в которой можно запускать маршруты (пример по ссылке).
Заключение
Если вы начинаете задействовать новые технологии, советую обращаться к опыту специалистов и обсуждать с ними технические вопросы. Их оперативные рекомендации помогут сэкономить время и быстрее научиться всему необходимому.
Читайте также:
- Kotlin Coroutines для Android — Прощай RxJava?
- Как работают обобщения в Kotlin
- Автоматизация создания файлов для нового экрана с плагином для Android Studio
Читайте нас в Telegram, VK и Дзен
Перевод статьи Gabriel Bronzatti Moro: An API Project From an Android Developer’s Perspective