Создание сайтов в Москве. Создание микросервисов с помощью Deno, Reno и PostgreSQL

 
 

В этом руководстве мы покажем вам, как создавать микросервисы с помощью Deno, и познакомим вас с Reno — тонкой библиотекой маршрутизации для Deno. Мы рассмотрим, как мы можем использовать эту новую платформу JavaScript для создания микрослужбы, предоставляющей конечные точки для работы с базой данных.

Deno — это среда выполнения JavaScript и TypeScript от создателя Node.js Райана Даля, целью которой является устранение некоторых недостатков последней технологии, таких как упрощение алгоритма поиска пути к модулю и более точное согласование основных API-интерфейсов с их эквивалентами на основе браузера. Несмотря на эти фундаментальные различия, потенциальные приложения Deno и Node.js в основном идентичны. Одна из основных сильных сторон Node заключается в создании HTTP-сервисов, то же самое можно сказать и о Deno.

Написание HTTP-серверов с помощьюstd/http

Прежде чем мы представим библиотеку маршрутизации или рассмотрим наш уровень доступа к данным, было бы полезно сделать шаг назад и построить простой HTTP-сервер с std/httpмодулем, который является частью стандартной библиотеки Deno. Если вы еще этого не сделали, установите Deno. В операционной системе типа Unix вы можете запустить:

$ curl -fsSL https://deno.land/x/install/install.sh | sh -s v1.3.0

Обратите внимание, что это руководство было разработано для версии 1.3.0 (и стандартной версии 0.65.0, как мы увидим позже), но любые более поздние версии 1.x, которые вы можете использовать, должны быть совместимы. В качестве альтернативы, если вы используете более старую версию Deno, вы можете обновить ее до 1.3.0 с помощью deno upgradeкоманды:

deno upgrade —version 1.3.0

Вы можете убедиться, что ожидаемая версия Deno была установлена ​​с расширением deno —version.

Теперь мы можем построить HTTP-сервер. Создайте каталог в своем обычном каталоге разработки с именем deno-hello-httpи откройте его в своем редакторе. Затем создайте файл с именем server.tsи используйте listenAndServeфункцию внутри std/httpдля создания нашего сервера:

import { listenAndServe } from «https: //deno.land/std@0.65.0/http/mod.ts»;

const BINDING = «:8000»;

console.log (`Listening on ${BINDING}... `) ;

await listenAndServe (BINDING, (req) => {

req.respond ({ body: «Hello world!» }) ;

}) ;

Советы по опыту разработчиков

Если вы используете VS Code, я настоятельно рекомендую официальное расширение Deno, которое обеспечивает поддержку алгоритма разрешения путей Deno. Кроме того, вы можете запустить deno cache server.tsдля установки зависимостей и их определений TypeScript, последнее служит бесценным руководством по API при написании кода.

Мы можем запустить наш сервер, запустив его deno run —allow-net server.tsв нашей оболочке. Обратите внимание на —allow-net флаг разрешений, предоставляющий нашей программе доступ к сети. После прослушивания порта 8000мы можем нацелить его с помощью HTTP-запроса:

$ curl -v http: //localhost:8000/; echo

> GET / HTTP/1.1

> Host: localhost:8000

> User-Agent: curl/7.58.0

> Accept: */*

>

< HTTP/1.1 200 OK

< content-length: 12

<

Hello world!

Большой! С помощью нескольких строк TypeScript мы смогли реализовать простой сервер. Тем не менее, на данный момент он не особенно хорошо представлен. Учитывая, что мы постоянно обслуживаем «Hello world!»нашу функцию обратного вызова, один и тот же ответ будет возвращен для любой конечной точки или метода HTTP. Если мы попадем на сервер с помощью POST /add, мы получим те же заголовки и тело:

$ curl -v -d '{}' http: //localhost:8000/add; echo

> POST /add HTTP/1.1

> Host: localhost:8000

> User-Agent: curl/7.58.0

> Accept: */*

> Content-Length: 2

> Content-Type: application/x-www-form-urlencoded

>

< HTTP/1.1 200 OK

< content-length: 12

<

Hello world!

Мы можем ограничить существующий ответ GET /, условно проверив urlи methodсвойства нашего reqпараметра обратного вызова:

import {

listenAndServe,

ServerRequest,

} from «https: //deno.land/std@0.65.0/http/mod.ts»;

const BINDING = «:8000»;

console.log (`Listening on ${BINDING}... `) ;

function notFound ({ method, url }: ServerRequest) {

return {

status: 404,

body: `No route found for ${method} ${url}`,

};

}

await listenAndServe (BINDING, (req) => {

const res = req.method === «GET» && req.url === «/»

? { body: «Hello world» }

: notFound (req) ;

req.respond (res) ;

}) ;

Если мы перезапустим наш сервер, мы должны заметить, что GET /он работает должным образом, но любой другой URL-адрес или метод приведет к HTTP 404:

$ curl -v -d '{}' http: //localhost:8000/add; echo

> POST /add HTTP/1.1

> Host: localhost:8000

> User-Agent: curl/7.58.0

> Accept: */*

> Content-Length: 2

> Content-Type: application/x-www-form-urlencoded

>

< HTTP/1.1 404 Not Found

< content-length: 28

<

No route found for POST /add

std/httpПомимо простых услуг

Начальная загрузка тривиальных HTTP-серверов с помощью Deno std/httpоказалась относительно простой. Как этот подход масштабируется для более сложных сервисов?

Давайте рассмотрим /messagesконечную точку, которая принимает и возвращает отправленные пользователем сообщения. Следуя подходу RESTful, мы можем определить поведение этой конечной точки и нашего сервиса в целом:

/messages

GET: возвращает сериализованный в формате JSON массив всех сообщений, хранящихся в памяти сервера.

POST: добавляет новое сообщение в массив в памяти

Все другие методы вернут HTTP 405 (метод не разрешен)

Все остальные URL-адреса будут возвращать HTTP 404 (не найдено).

Давайте обновим наш существующий server.tsмодуль, чтобы он соответствовал нашей новой спецификации сервиса:

import {

listenAndServe,

ServerRequest,

} from «https: //deno.land/std@0.65.0/http/mod.ts»;

interface MessagePayload {

message: string;

}

const BINDING = «:8000»;

const decoder = new TextDecoder () ;

const messages: string[] = [];

function jsonResponse (body: TBody, status = 200) {

return {

status,

headers: new Headers ({

«Content-Type»: «application/json»,

}),

body: JSON.stringify (body),

};

}

function textResponse (body: string, status = 200) {

return {

status,

headers: new Headers ({

«Content-Type»: «text/plain»,

}),

body,

};

}

async function addMessage ({ body }: ServerRequest) {

const { message }: MessagePayload = JSON.parse (

decoder.decode (await Deno.readAll (body)),

) ;

messages.push (message) ;

return jsonResponse ({ success: true }, 201) ;

}

function getMessages () {

return jsonResponse (messages) ;

}

function methodNotAllowed ({ method, url }: ServerRequest) {

return textResponse (

`${method} method not allowed for resource ${url}`,

405,

) ;

}

function notFound ({ url }: ServerRequest) {

return textResponse (`No resource found for ${url}`, 404) ;

}

function internalServerError ({ message }: Error) {

return textResponse (message, 500) ;

}

console.log (`Listening on ${BINDING}... `) ;

await listenAndServe (BINDING, async (req) => {

let res = notFound (req) ;

try {

if (req.url === «/messages») {

switch (req.method) {

case «POST»:

res = await addMessage (req) ;

break;

case «GET»:

res = getMessages () ;

break;

default:

res = methodNotAllowed (req) ;

}

}

} catch (e) {

res = internalServerError (e) ;

}

req.respond (res) ;

}) ;

Перезапустите сервер и убедитесь, что GET /messagesон возвращает application/jsonответ с пустым массивом JSON в качестве тела. Затем мы можем проверить, что добавление сообщения работает, сделав POSTзапрос /messagesс действительной полезной нагрузкой и впоследствии получив сообщения:

$ curl -v -H «Content-Type: application/json» -d '{ «message»: «Hello!» }' http: //localhost:8000/messages; echo

< HTTP/1.1 201 Created

< content-length: 16

< content-type: application/json

<

{«success»: true}

$ curl -v http: //localhost:8000/messages; echo

< HTTP/1.1 200 OK

< content-length: 10

< content-type: application/json

<

["Hello!"]

Объявление маршрутов с Reno

Учитывая, что наш сервис предоставляет только одну конечную точку, код остается довольно ненавязчивым. Однако если бы он охватывал множество конечных точек, то наш код обработки маршрутов вскоре стал бы неуправляемым:

if (req.url === «/messages») {

switch (req.method) {

case «POST»:

res = await addMessage (req) ;

break;

case «GET»:

// Route params e.g. /messages/ade25ef

const [, id] = req.url.match (/^\/messages\/ ([a-z0−9]*) $/) || [];

res = id? getMessage (id): getMessages () ;

break;

default:

res = methodNotAllowed (req) ;

}

} else if (req.url === «/topics») {

switch (req.method) {

case «GET»:

res = getTopics () ;

break;

default:

res = methodNotAllowed (req) ;

}

} else if (req.url === «/users») {

//...etc

}

Мы, безусловно, могли бы структурировать этот код, чтобы сделать его более декларативным, например, определить Mapфункции обработчика маршрута, которые соответствуют конкретному пути, но тем не менее нам пришлось бы самим обрабатывать реализацию маршрутизации, расширяя поиск маршрута, анализ пути и запрос. параметры и вложенные маршруты. Даже с самым хорошо структурированным кодом это непростая задача, а в бизнес-контексте она съела бы драгоценное время разработки.

В течение последнего года я работал над Reno, библиотекой маршрутизации для std/httpсервера, которая обрабатывает и абстрагирует большую часть этой сложности, позволяя нам сосредоточиться на основной логике наших приложений. Используя предоставленные сопутствующие функции маршрутизатора, давайте перестроим нашу службу сообщений:

import {

listenAndServe,

ServerRequest,

} from «https: //deno.land/std@0.65.0/http/mod.ts»;

import {

createRouter,

createRouteMap,

forMethod,

withJsonBody,

jsonResponse,

textResponse,

ProcessedRequest,

NotFoundError,

} from «https: //deno.land/x/reno@v1.3.0/reno/mod.ts»;

interface MessagePayload {

message: string;

}

const BINDING = «:8000»;

const messages: string[] = [];

async function addMessage (

{ body: { message } }: ProcessedRequest,

) {

messages.push (message) ;

return jsonResponse ({ success: true }, {}, 201) ;

}

function getMessages () {

return jsonResponse (messages) ;

}

function notFound ({ url }: ServerRequest) {

return textResponse (`No resource found for ${url}`, {}, 404) ;

}

function internalServerError ({ message }: Error) {

return textResponse (message, {}, 500) ;

}

const routes = createRouteMap ([

[

«/messages»,

forMethod ([

[«GET», getMessages],

[«POST», withJsonBody (addMessage) ],

]),

],

]) ;

const router = createRouter (routes) ;

console.log (`Listening on ${BINDING}... `) ;

await listenAndServe (BINDING, async (req) => {

try {

req.respond (await router (req));

} catch (e) {

req.respond (

e instanceof NotFoundError? notFound (req): internalServerError (e),

) ;

}

}) ;

Если вы перезапустите сервер и сделаете то же самое GETи POSTзапросы к /messages, мы заметим, что основные функции остаются нетронутыми. Чтобы повторить сложность, с которой справляется Reno, вот как будет выглядеть пример с несколькими конечными точками:

const routes = createRouteMap ([

[

/^\/messages\/ ([a-z0−9]*) $/,

forMethod ([

[«GET», ({ routeParams: [id] }) => id? getMessage (id): getMessages],

[«POST», withJsonBody (addMessage) ],

]),

],

[«/topics», getTopics],

[«/users», getUsers],

]) ;

Поскольку Reno предоставляет встроенный синтаксический анализ пути и обработку HTTP-методов из коробки, среди других своих функций, нам нужно только заняться объявлением наших конечных точек и логикой ответа на запросы, которые они могут получить.

Один фундаментальный принцип Reno, на который стоит обратить внимание, заключается в том, что он представляет собой маршрутизатор как функцию. То есть const response = await router (request). В отличие от полноценных серверных фреймворков, которые часто берут на себя ответственность за загрузку HTTP-сервера и управление его жизненным циклом, Reno занимается только маршрутизацией запросов, которую он выполняет с помощью автономного вызова функции; это облегчает его внедрение, а также интеграцию с существующими услугами Deno.

Создание микросервисов с Reno

Учитывая небольшой API Reno, он хорошо подходит для разработки микросервисов. В этом случае мы собираемся создать микросервис для сообщений в блоге с Deno и Reno, поддерживаемый базой данных PostgreSQL (мы будем использовать великолепный deno-postgres для запросов к нашей базе данных из Deno). Наш сервис предоставит единую /postsконечную точку, которая поддерживает ряд операций:

GET /posts: извлекает метаданные для всех сообщений в базе данных.

GET /posts/: извлекает метаданные и содержимое поста с заданным UUID

POST /posts: добавляет новый пост в базу данных

PATCH /posts/: заменяет содержимое сообщения заданным UUID

Создание полноценного микросервиса может показаться сложной задачей для одного учебника, но я любезно предоставил существенный шаблон, который содержит настройку Docker Compose и предварительно написанные сценарии и запросы к базе данных. Для начала убедитесь, что вы установили Docker и Docker Compose, а затем [ клонируйте микросервис блога Reno, специально проверив sitepoint-boilerplateветку:

$ git clone —branch sitepoint-boilerplate https://github.com/reno-router/blog-microservice.git

Откройте blog-microserviceпапку в выбранном вами редакторе. Прежде чем мы реализуем наш первый маршрут, я рассмотрю некоторые ключевые каталоги и файлы на высоком уровне:

data: содержит сценарии SQL, которые будут запускаться при создании контейнера базы данных, определяя таблицы нашего приложения и заполняя их некоторыми начальными данными.

service/blog_service.ts: предоставляет методы для получения, создания и обновления сообщений, хранящихся в базе данных.

service/db_service.ts: общая абстракция базы данных, которая находится поверх deno-postgres и бесплатно обрабатывает пул соединений и транзакции.

service/queries.ts: предопределенные запросы Postgres для наших различных операций с базой данных; служба блогов передает их службе БД и пересылает результаты в пригодном для использования формате вызывающей стороне. Эти запросы параметризуются, значения которых deno-postgres автоматически санирует.

service/server.ts: точка входа нашего сервера.

deps.ts: централизованный модуль, содержащий все внешние зависимости, что позволяет поддерживать их в одной точке. Эта практика распространена во всех проектах Deno и одобрена официальным руководством.

Dockerfile: объявляет наш производственный контейнер Docker, который будет устанавливать зависимости нашего проекта во время сборки, что значительно сокращает время холодного запуска.

Dockerfile.local: объявляет наш контейнер Docker для разработки, используя Denon для автоматического перезапуска Deno при каждом изменении нашего исходного кода.

docker-compose.yml: конфигурация Docker Compose, включающая как наш контейнер для разработки, так и контейнер Postgres, в котором запускаются наши SQL-скрипты, что значительно сокращает количество необходимых шагов для запуска нашего проекта.

Давайте создадим маршруты нашего приложения. В serviceпапке создайте новый файл с именем routes.ts. Заполните его этими импортами, которые нам скоро понадобятся:

import {

createRouteMap,

jsonResponse,

forMethod,

DBPool,

uuidv4,

} from «.../deps.ts»;

import createBlogService from «. /blog_service.ts»;

import createDbService from «. /db_service.ts»;

Далее давайте создадим экземпляр нашего пула соединений с базой данных. Обратите внимание, что с помощью Object.fromEntries, мы можем создать объект параметров, требуемый deno-postgres, относительно лаконичным образом:

function createClientOpts () {

return Object.fromEntries ([

[«hostname», «POSTGRES_HOST„],

[“user», «POSTGRES_USER„],

[“password», «POSTGRES_PASSWORD„],

[“database», «POSTGRES_DB»],

].map (([key, envVar]) => [key, Deno.env.get (envVar) ]));

}

function getPoolConnectionCount () {

return Number.parseInt (Deno.env.get («POSTGRES_POOL_CONNECTIONS») || «1», 10) ;

}

const dbPool = new DBPool (createClientOpts (), getPoolConnectionCount ());

С помощью нашего созданного пула соединений мы можем создать нашу базу данных и службы блогов:

const blogService = createBlogService (

createDbService (dbPool),

uuidv4.generate,

) ;

Теперь давайте напишем обработчик маршрута для получения всех сообщений в базе данных:

async function getPosts () {

const res = await blogService.getPosts () ;

return jsonResponse (res) ;

}

Чтобы привязать наш обработчик к GET /posts, нам нужно объявить карту маршрута и экспортировать ее:

const routes = createRouteMap ([

[«/posts», forMethod ([

[«GET», getPosts],

]) ],

]) ;

export default routes;

В итоге routes.tsдолжно получиться так:

import {

createRouteMap,

jsonResponse,

forMethod,

DBPool,

uuidv4,

} from «.../deps.ts»;

import createBlogService from «. /blog_service.ts»;

import createDbService from «. /db_service.ts»;

function createClientOpts () {

return Object.fromEntries ([

[«hostname», «POSTGRES_HOST„],

[“user», «POSTGRES_USER„],

[“password», «POSTGRES_PASSWORD„],

[“database», «POSTGRES_DB»],

].map (([key, envVar]) => [key, Deno.env.get (envVar) ]));

}

function getPoolConnectionCount () {

return Number.parseInt (Deno.env.get («POSTGRES_POOL_CONNECTIONS») || «1», 10) ;

}

const dbPool = new DBPool (createClientOpts (), getPoolConnectionCount ());

const blogService = createBlogService (

createDbService (dbPool),

uuidv4.generate,

) ;

async function getPosts () {

const res = await blogService.getPosts () ;

return jsonResponse (res) ;

}

const routes = createRouteMap ([

[«/posts», forMethod ([

[«GET», getPosts],

]) ],

]) ;

export default routes;

Чтобы пересылать запросы нашему обработчику, нам нужно обновить существующий server.tsмодуль. Добавьте createRouterк привязкам, импортированным из deps.ts:

import {

listenAndServe,

ServerRequest,

textResponse,

createRouter,

} from «.../deps.ts»;

Под этим оператором нам нужно импортировать наши маршруты:

import routes from «. /routes.ts»;

Чтобы создать маршрутизатор нашего сервиса, вызовите createRouterфункцию над сообщением о прослушивании сервера, передав наши маршруты в качестве единственного аргумента:

const router = createRouter (routes) ;

Наконец, чтобы перенаправить входящие запросы на наш маршрутизатор и вернуть предполагаемый ответ, давайте вызовем маршрутизатор в tryблоке обратного вызова нашего сервера:

try {

const res = await router (req) ;

return req.respond (res) ;

}

Теперь мы готовы запустить наше приложение, но остался последний шаг. Нам нужно переименовать.env.sampleфайл в.env. У него есть.sampleсуффикс, указывающий на то, что он не содержит реальных, конфиденциальных значений, но для начала мы тем не менее можем использовать их дословно:

$ mv.env.sample.env

С помощью swift docker-compose upмы должны увидеть, как оживают контейнеры базы данных и сервисов, причем последний в конечном итоге прослушивает порт 8000:

$ docker-compose up

# [... ]

db_1 | 2020-08-16 22:04:50.314 UTC [1] LOG: database system is ready to accept connections

# [... ]

api_1 | Listening for requests on:8000...

После привязки к этому порту мы должны убедиться, что наша конечная точка работает. Он должен возвращать идентификатор, заголовок и теги для каждого сообщения в базе данных, в настоящее время заполненные исходными данными:

# jq is like sed for JSON data:

# https://stedolan.github.io/jq/

$ curl http: //localhost:8000/posts | jq

[

{

«id»: «006a8213−8aac-47e2-b728-b0e2c07ddaf6»,

«title»: «Go’s generics experimentation tool»,

«author»: {

«id»: «c9e69690-9246-41bf-b912−0c6190f64f1f»,

«name»: «Joe Bloggs»

},

«tags»: [

{

«id»: «f9076c31−69eb-45cf-b51c-d7a1b6e3fe0c»,

«name»: «Go»

}

]

},

{

«id»: «16f9d2b0-baf9-4618-a230-d9b95ab75fa8»,

«title»: «Deno 1.3.0 released»,

«author»: {

«id»: «91ef4450−97ff-44da-8b1d-f1560e9d10cc»,

«name»: «James Wright»

},

«tags»: [

{

«id»: «21c1ac3a−9c1b−4be1-be50−001b44cf84d1»,

«name»: «JavaScript»

},

{

«id»: «ac9c2f73−6f11-470f-b8a7−9930dbbf137a»,

«name»: «TypeScript»

},

{

«id»: «c35defc4−42f1-43b9-a181-a8f12b8457f1»,

«name»: «Deno»

},

{

«id»: «d7c2f180−18d6-423e-aeda-31c4a3a7ced1»,

«name»: «Rust»

}

]

}

]

Получение содержимого публикации

Следующей операцией, которую необходимо реализовать, является GET /posts/. Учитывая, что мы уже обрабатываем GET /posts, мы можем внести минимальный набор изменений для получения отдельных сообщений по их идентификатору. Прежде всего, давайте настроим «/posts„привязку пути на нашей карте маршрутов, чтобы ввести сегмент пути с подстановочными знаками:

const routes = createRouteMap ([

[“/posts/*», forMethod ([

[«GET», getPosts],

]) ],

]) ;

В дополнение к регулярным выражениям Reno позволяет использовать строковые пути с подстановочными знаками ('*'), которые будут захвачены и представлены через routeParamsсвойство запроса. Хотя они не так специфичны, как регулярные выражения, их, возможно, легче читать, и в основном они служат для достижения той же цели. Давайте обновим getPostsобработчик маршрута, чтобы определить наличие параметра пути и получить отдельную публикацию из службы блогов, если она присутствует (AugmentedRequestтип можно импортировать из deps.ts):

async function getPosts ({ routeParams: [id] }: AugmentedRequest) {

const res = await (id? blogService.getPost (id): blogService.getPosts ());

return jsonResponse (res) ;

}

Обратите внимание, что routeParamsэто линейно упорядоченный массив, в котором каждый элемент ссылается на параметр пути в том порядке, в котором он объявлен. Таким образом, в нашем случае мы можем убедиться, что первый элемент всегда относится к идентификатору сообщения. После сохранения наших изменений Denon обнаружит изменения и перезапустит Deno, а вызов GET /postsс последующим идентификатором одного из наших сообщений должен вернуть его метаданные и содержимое:

$ curl http: //localhost:8000/posts/16f9d2b0-baf9-4618-a230-d9b95ab75fa8 | jq

{

«id»: «16f9d2b0-baf9-4618-a230-d9b95ab75fa8»,

«title»: «Deno 1.3.0 released»,

«contents»: «This release includes new flags to various Deno commands and implements the W3C FileReader API, amongst other enhancements and fixes.»,

«author»: {

«id»: «91ef4450−97ff-44da-8b1d-f1560e9d10cc»,

«name»: «James Wright»

},

«tags»: [

{

«id»: «21c1ac3a−9c1b−4be1-be50−001b44cf84d1»,

«name»: «JavaScript»

},

{

«id»: «ac9c2f73−6f11-470f-b8a7−9930dbbf137a»,

«name»: «TypeScript»

},

{

«id»: «c35defc4−42f1-43b9-a181-a8f12b8457f1»,

«name»: «Deno»

},

{

«id»: «d7c2f180−18d6-423e-aeda-31c4a3a7ced1»,

«name»: «Rust»

}

]

}

Работа с несуществующими сообщениями

Расширение нашей GET /postsоперации для получения отдельного сообщения по его идентификатору привело к ошибке. Запросим содержимое поста по несуществующему ID:

$ curl -v http: //localhost:8000/posts/b801087e-f1c9−4b1e−9e0c−70405b685e86

> GET /posts/b801087e-f1c9−4b1e−9e0c−70405b685e86 HTTP/1.1

> Host: localhost:8000

> User-Agent: curl/7.54.0

> Accept: */*

>

< HTTP/1.1 200 OK

< content-length: 0

< content-type: application/json

<

Поскольку blogService.getPost (id) возвращается undefined, когда сообщение с заданным идентификатором не может быть найдено, наш текущий обработчик возвращает ответ HTTP 200 с пустым телом. Было бы предпочтительнее сообщить об этой ошибке инициатору запроса. Чтобы getPostsфункция оставалась читаемой, давайте перенесем blogService.getPost (id) вызов в отдельную функцию, в которой мы выдадим ошибку, если полученное сообщение имеет вид undefined. Тип BlogServiceможно импортировать из blog_service.ts:

async function getPost (blogService: BlogService, id: string) {

const res = await blogService.getPost (id) ;

if (! res) {

throw new Error (`Post not found with ID ${id}`) ;

}

return res;

}

async function getPosts ({ routeParams: [id] }: AugmentedRequest) {

const res = await (id? getPost (blogService, id): blogService.getPosts ());

return jsonResponse (res) ;

}

Если мы теперь запросим публикацию, которой не существует, нам будет предоставлен ответ об ошибке:

$ curl -v http: //localhost:8000/posts/b801087e-f1c9−4b1e−9e0c−70405b685e86; echo

> GET /posts/b801087e-f1c9−4b1e−9e0c−70405b685e86 HTTP/1.1

> Host: localhost:8000

> User-Agent: curl/7.54.0

> Accept: */*

>

< HTTP/1.1 500 Internal Server Error

< content-length: 59

< content-type: text/plain

<

Post not found with ID b801087e-f1c9−4b1e−9e0c−70405b685e86

Это, безусловно, улучшение, но, возможно, код состояния не точен. Этот ответ является результатом не ошибки приложения, а того, что пользователь указал отсутствующий пост. В этом случае лучше подойдет HTTP 404. Над getPostфункцией мы можем определить собственный класс ошибок, который будет выдаваться, когда запись не найдена:

export class PostNotFoundError extends Error {

constructor (id: string) {

super (`Post not found with ID ${id}`) ;

}

}

Затем в теле getPostмы можем бросить это вместо ванильного Errorэкземпляра:

async function getPost (blogService: BlogService, id: string) {

const res = await blogService.getPost (id) ;

if (! res) {

throw new PostNotFoundError (`Post not found with ID ${id}`) ;

}

return res;

}

Преимущество создания пользовательской ошибки заключается в том, что мы можем обслужить конкретный ответ, когда он будет обнаружен. В server.ts, давайте обновим switchоператор в mapToErrorResponseфункции, чтобы возвращать вызов, notFound () когда PostNotFoundErrorпроисходит наше:

function mapToErrorResponse (e: Error) {

switch (e.constructor) {

case PostNotFoundError:

return notFound (e) ;

default:

return serverError (e) ;

}

}

После повторной попытки предыдущего запроса мы должны увидеть, что получили HTTP 404:

$ curl -v http: //localhost:8000/posts/b801087e-f1c9−4b1e−9e0c−70405b685e86; echo

> GET /posts/b801087e-f1c9−4b1e−9e0c−70405b685e86 HTTP/1.1

> Host: localhost:8000

> User-Agent: curl/7.54.0

> Accept: */*

>

< HTTP/1.1 404 Not Found

< content-length: 82

< content-type: text/plain

<

Post not found with ID Post not found with ID b801087e-f1c9−4b1e−9e0c−70405b685e86

Мы также должны добавить Reno NotFoundErrorв этот случай, что также приведет к обслуживанию HTTP 404, если маршрут запроса не существует:

switch (e.constructor) {

case PostNotFoundError:

case NotFoundError:

return notFound (e) ;

default:

return serverError (e) ;

}

Мы можем следовать этому шаблону для обработки других типов ошибок в нашем приложении. Например, полная служба выдает HTTP 400 (неверный запрос), когда пользователь создает ресурс с недопустимым UUID.

Добавление новых сообщений в базу данных

До сих пор операции, которые мы реализовывали, читали сообщения из базы данных. Как насчет создания новых постов? Мы можем добавить обработчик маршрута для этого, но сначала нам нужно импортировать withJsonBodyиз deps.tsв routes.ts:

import {

createRouteMap,

jsonResponse,

forMethod,

DBPool,

uuidv4,

AugmentedRequest,

withJsonBody,

} from «.../deps.ts»;

Мы также должны импортировать CreatePostPayloadинтерфейс из blog_service.ts, который нам вскоре понадобится:

import createBlogService, {

BlogService,

CreatePostPayload,

} from «. /blog_service.ts»;

withJsonBody— это обработчик маршрутов более высокого порядка, который предполагает, что базовое тело запроса представляет собой сериализованную строку JSON, и анализирует ее для нас. Он также поддерживает общий параметр, который позволяет нам утверждать тип тела. Давайте используем его для определения нашего addPostобработчика:

const addPost = withJsonBody (

async function addPost ({ body }) {

const id = await blogService.createPost (body) ;

return jsonResponse ({ id }) ;

},

) ;

Затем мы должны зарегистрировать обработчик в нашей карте маршрутов:

const routes = createRouteMap ([

[

«/posts/*»,

forMethod ([

[«GET», getPosts],

[«POST», addPost],

]),

],

]) ;

Чтобы проверить, POST /postsработает ли наша операция, мы можем сделать этот запрос с действительной полезной нагрузкой создания сообщения:

$ curl -H «Content-Type: application/json» -d '{

«authorId»: «91ef4450−97ff-44da-8b1d-f1560e9d10cc»,

«title»: «New post»,

«contents»: «This was submitted via our new API endpoint!»,

«tagIds»: [«6a7e1f4d−7fca-4573-b138-f2cba0163077», «f9076c31−69eb-45cf-b51c-d7a1b6e3fe0c»]

}' http: //localhost:8000/posts | jq

{

«id»: «586bb055-cea64d56-8d8d−1856e8f8e5eb»

}

Затем мы можем убедиться, что это было успешно сохранено в нашей базе данных, запросив сообщение по сгенерированному UUID:

$ curl http: //localhost:8000/posts/586bb055-cea64d56-8d8d−1856e8f8e5eb | jq

{

«id»: «586bb055-cea64d56-8d8d−1856e8f8e5eb»,

«title»: «New post»,

«contents»: «This was submitted via our new API endpoint!»,

«author»: {

«id»: «91ef4450−97ff-44da-8b1d-f1560e9d10cc»,

«name»: «James Wright»

},

«tags»: [

{

«id»: «6a7e1f4d−7fca-4573-b138-f2cba0163077»,

«name»: «C#»

},

{

«id»: «f9076c31−69eb-45cf-b51c-d7a1b6e3fe0c»,

«name»: «Go»

}

]

}

Редактирование существующих сообщений

Чтобы завершить наш сервис, мы собираемся реализовать PATCH /posts/маршрут, который позволяет заменять содержимое сообщения. Начнем с импорта EditPostPayloadинтерфейса из blog_service.ts:

import createBlogService, {

BlogService,

CreatePostPayload,

EditPostPayload,

} from «. /blog_service.ts»;

Затем мы должны добавить функцию обработки маршрута с именем editPost:

const editPost = withJsonBody (

async function editPost ({ body: { contents }, routeParams: [id] }) {

const rowCount = await blogService.editPost (id, contents) ;

if (rowCount === 0) {

throw new PostNotFoundError (id) ;

}

return jsonResponse ({ id }) ;

},

) ;

В заключение давайте добавим обработчик к нашим маршрутам:

const routes = createRouteMap ([

[

«/posts/*»,

forMethod ([

[«GET», getPosts],

[«POST», addPost],

[«PATCH», editPost],

]),

],

]) ;

Мы можем убедиться, что наш обработчик работает, обновив содержимое записи, которую мы создали в предыдущем разделе:

$ curl -X PATCH -H «Content-Type: application/json» -d '{

«contents»: «This was edited via our new API endpoint!»

}' http: //localhost:8000/posts/586bb055-cea64d56-8d8d−1856e8f8e5eb | jq

{

«id»: «586bb055-cea64d56-8d8d−1856e8f8e5eb»

}

$ curl http: //localhost:8000/posts/586bb055-cea64d56-8d8d−1856e8f8e5eb | jq.contents

«This was edited via our new API endpoint!»

Вызов GET /postsоперации также должен продемонстрировать, что в базе данных не было сохранено никаких дополнительных сообщений.

Следующие шаги

Мы создали хорошо спроектированную и удобную в сопровождении службу, но есть еще дополнительные шаги, которые повысят надежность и безопасность нашей службы, такие как проверка входящих полезных данных и авторизация запросов POSTи. PUTКроме того, мы могли бы написать несколько модульных тестов для наших обработчиков маршрутов. Учитывая, что они фактически являются чистыми функциями (то есть они производят детерминированный ответ для заданного ввода, а побочные эффекты необязательны), мы можем добиться этого с относительно небольшими накладными расходами:

Deno.test (

«getPosts route handler should retrieve the post for the given ID from the blog service»,

async () => {

const id = «post ID»;

const post = {

id,

title: «Test Post»,

author: {

id: «author ID»,

name: «James Wright»,

},

tags: [

{ id: «tag ID», name: «JavaScript» },

{ id: «tag ID», name: «TypeScript» },

],

};

const blogService = {

getPost: sinon.stub ().resolves (post),

getPosts: sinon.stub ().resolves (),

};

const getPosts = createGetPostsHandler (blogService) ;

const response = await getPosts ({ routeParams: [id] }) ;

assertResponsesAreEqual (response, jsonResponse (post));

assertStrictEquals (blogService.getPost.callCount, 1) ;

assertStrictEquals (blogService.getPosts.callCount, 0) ;

},

) ;

Обратите внимание, что мы используем частичное приложение для внедрения службы блога-заглушки в обработчик маршрута, который мы можем соответствующим образом обновить:

export function createGetPostsHandler (

blogService: Pick,,>

) {

return async function getPosts (

{ routeParams: [id] }: Pick,,>

) {

const res = await (id? getPost (blogService, id): blogService.getPosts ());

return jsonResponse (res) ;

};

}

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

Резюме

Создание небольших HTTP-сервисов std/httpвозможно, но управление дополнительными конечными точками, выделенная логика для конкретных методов HTTP и обработка ошибок могут стать обременительными по мере роста наших приложений. Reno скрывает от нас эти сложности, позволяя нам сосредоточиться на основной бизнес-логике наших микросервисов. Учитывая структуру функций обработчика маршрутов, приложения, которые маршрутизируются с помощью Reno, по своей природе поддаются модульному тестированию и могут легко интегрироваться с существующими проектами Deno.

Тем не менее, более крупные или более сложные сервисы могут выиграть от полной инфраструктуры, такой как Oak. Однако для микросервисов Reno предоставляет очень маленькую ненавязчивую поверхность API, которая позволяет им масштабироваться по мере роста наших бизнес-требований.

3D-печать5GABC-анализAndroidAppleAppStoreAsusCall-центрChatGPTCRMDellDNSDrupalExcelFacebookFMCGGoogleHuaweiInstagramiPhoneLinkedInLinuxMagentoMicrosoftNvidiaOpenCartPlayStationPOS материалPPC-специалистRuTubeSamsungSEO-услугиSMMSnapchatSonyStarlinkTikTokTwitterUbuntuUp-saleViasatVPNWhatsAppWindowsWordPressXiaomiYouTubeZoomАвдеевкаАктивные продажиАкцияАлександровск ЛНРАлмазнаяАлчевскАмвросиевкаАнализ конкурентовАнализ продажАнтимерчандайзингАнтрацитАртемовскАртемовск ЛНРАссортиментная политикаБелгородБелицкоеБелозерскоеБердянскБизнес-идеи (стартапы)БрендБрянкаБукингВахрушевоВендорВидеоВикипедияВирусная рекламаВирусный маркетингВладивостокВнутренние продажиВнутренний маркетингВолгоградВолновахаВоронежГорловкаГорнякГорскоеДебальцевоДебиторкаДебиторская задолженностьДезинтермедитацияДзержинскДивизионная система управленияДизайнДимитровДирект-маркетингДисконтДистрибьюторДистрибьюцияДобропольеДокучаевскДоменДружковкаЕкатеринбургЕнакиевоЖдановкаЗапорожьеЗимогорьеЗолотоеЗоринскЗугрэсИжевскИловайскИрминоКазаньКалининградКировскКировскоеКомсомольскоеКонстантиновкаКонтент-маркетингКонтент-планКопирайтингКраматорскКрасноармейскКрасногоровкаКраснодарКраснодонКраснопартизанскКрасный ЛиманКрасный ЛучКременнаяКураховоКурскЛисичанскЛуганскЛутугиноМакеевкаМариупольМаркетингМаркетинговая информацияМаркетинговые исследованияМаркетинговый каналМаркетинг услугМаркетологМарьинкаМедиаМелекиноМелитопольМенеджментМерчандайзерМерчандайзингМиусинскМолодогвардейскМоскваМоспиноНижний НовгородНиколаевНиколаевкаНишевой маркетингНовоазовскНовогродовкаНоводружескНовосибирскНумерическая дистрибьюцияОдессаОмскОтдел маркетингаПартизанский маркетингПервомайскПеревальскПетровскоеПлата за кликПоисковая оптимизацияПопаснаяПравило ПаретоПривольеПрогнозирование продажПродвижение сайтов в ДонецкеПроизводство видеоПромоПромоушнПрямой маркетингРабота для маркетологаРабота для студентаРазработка приложенийРаспродажаРегиональные продажиРекламаРеклама на асфальтеРемаркетингРетро-бонусРибейтРитейлРовенькиРодинскоеРостов-на-ДонуРубежноеСамараСанкт-ПетербургСаратовСватовоСвердловскСветлодарскСвятогорскСевастопольСеверодонецкСеверскСедовоСейлз промоушнСелидовоСимферопольСинергияСколковоСлавянскСнежноеСоздание сайтов в ДонецкеСоледарСоциальные сетиСочиСтаробельскСтаробешевоСтахановСтимулирование сбытаСуходольскСчастьеТелемаркетингТельмановоТираспольТорговый представительТорезТрейд маркетингТрейд промоушнТюменьУглегорскУгледарУкраинскХабаровскХарцызскХерсонХостингЦелевая аудиторияЦифровой маркетингЧасов ЯрЧелябинскШахтерскЮжно-СахалинскЮнокоммунаровскЯндексЯсиноватая