В этом руководстве мы покажем вам, как создавать микросервисы с помощью Deno, и познакомим вас с Reno — тонкой библиотекой маршрутизации для Deno. Мы рассмотрим, как мы можем использовать эту новую платформу JavaScript для создания микрослужбы, предоставляющей конечные точки для работы с базой данных.
Deno — это среда выполнения JavaScript и TypeScript от создателя Node.js Райана Даля, целью которой является устранение некоторых недостатков последней технологии, таких как упрощение алгоритма поиска пути к модулю и более точное согласование основных
Написание
Прежде чем мы представим библиотеку маршрутизации или рассмотрим наш уровень доступа к данным, было бы полезно сделать шаг назад и построить простой
$ 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.
Теперь мы можем построить
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 —
$ curl -v http: //localhost:8000/; echo
> GET / HTTP/1.1
> Host: localhost:8000
>
> Accept: */*
>
< HTTP/1.1 200 OK
<
<
Hello world!
Большой! С помощью нескольких строк TypeScript мы смогли реализовать простой сервер. Тем не менее, на данный момент он не особенно хорошо представлен. Учитывая, что мы постоянно обслуживаем «Hello world!»нашу функцию обратного вызова, один и тот же ответ будет возвращен для любой конечной точки или метода HTTP. Если мы попадем на сервер с помощью POST /add, мы получим те же заголовки и тело:
$ curl -v -d '{}' http: //localhost:8000/add; echo
> POST /add HTTP/1.1
> Host: localhost:8000
>
> Accept: */*
>
>
>
< HTTP/1.1 200 OK
<
<
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 /он работает должным образом, но любой другой
$ curl -v -d '{}' http: //localhost:8000/add; echo
> POST /add HTTP/1.1
> Host: localhost:8000
>
> Accept: */*
>
>
>
< HTTP/1.1 404 Not Found
<
<
No route found for POST /add
std/httpПомимо простых услуг
Начальная загрузка тривиальных
Давайте рассмотрим /messagesконечную точку, которая принимает и возвращает отправленные пользователем сообщения. Следуя подходу RESTful, мы можем определить поведение этой конечной точки и нашего сервиса в целом:
/messages
GET: возвращает сериализованный в формате JSON массив всех сообщений, хранящихся в памяти сервера.
POST: добавляет новое сообщение в массив в памяти
Все другие методы вернут HTTP 405 (метод не разрешен)
Все остальные
Давайте обновим наш существующий 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 ({
«
}),
body: JSON.stringify (body),
};
}
function textResponse (body: string, status = 200) {
return {
status,
headers: new Headers ({
«
}),
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 «
< HTTP/1.1 201 Created
<
<
<
{«success»: true}
$ curl -v http: //localhost:8000/messages; echo
< HTTP/1.1 200 OK
<
<
<
["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\/ ([
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
]),
],
]) ;
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\/ ([
forMethod ([
[«GET», ({ routeParams: [id] }) => id? getMessage (id): getMessages],
[«POST», withJsonBody
]),
],
[«/topics», getTopics],
[«/users», getUsers],
]) ;
Поскольку Reno предоставляет встроенный синтаксический анализ пути и обработку
Один фундаментальный принцип Reno, на который стоит обратить внимание, заключается в том, что он представляет собой маршрутизатор как функцию. То есть const response = await router (request). В отличие от полноценных серверных фреймворков, которые часто берут на себя ответственность за загрузку
Создание микросервисов с Reno
Учитывая небольшой API Reno, он хорошо подходит для разработки микросервисов. В этом случае мы собираемся создать микросервис для сообщений в блоге с Deno и Reno, поддерживаемый базой данных PostgreSQL (мы будем использовать великолепный
GET /posts: извлекает метаданные для всех сообщений в базе данных.
GET /posts/
POST /posts: добавляет новый пост в базу данных
PATCH /posts/
Создание полноценного микросервиса может показаться сложной задачей для одного учебника, но я любезно предоставил существенный шаблон, который содержит настройку Docker Compose и предварительно написанные сценарии и запросы к базе данных. Для начала убедитесь, что вы установили Docker и Docker Compose, а затем [ клонируйте микросервис блога Reno, специально проверив
$ git clone —branch
Откройте
data: содержит сценарии SQL, которые будут запускаться при создании контейнера базы данных, определяя таблицы нашего приложения и заполняя их некоторыми начальными данными.
service/blog_service.ts: предоставляет методы для получения, создания и обновления сообщений, хранящихся в базе данных.
service/db_service.ts: общая абстракция базы данных, которая находится поверх
service/queries.ts: предопределенные запросы Postgres для наших различных операций с базой данных; служба блогов передает их службе БД и пересылает результаты в пригодном для использования формате вызывающей стороне. Эти запросы параметризуются, значения которых
service/server.ts: точка входа нашего сервера.
deps.ts: централизованный модуль, содержащий все внешние зависимости, что позволяет поддерживать их в одной точке. Эта практика распространена во всех проектах Deno и одобрена официальным руководством.
Dockerfile: объявляет наш производственный контейнер Docker, который будет устанавливать зависимости нашего проекта во время сборки, что значительно сокращает время холодного запуска.
Dockerfile.local: объявляет наш контейнер Docker для разработки, используя Denon для автоматического перезапуска Deno при каждом изменении нашего исходного кода.
Давайте создадим маршруты нашего приложения. В 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, мы можем создать объект параметров, требуемый
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
$
# [... ]
db_1 |
# [... ]
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−
«title»: «Go’s generics experimentation tool»,
«author»: {
«id»: «
«name»: «Joe Bloggs»
},
«tags»: [
{
«id»: «f9076c31−
«name»: «Go»
}
]
},
{
«id»: «
«title»: «Deno 1.3.0 released»,
«author»: {
«id»: «91ef4450−
«name»: «James Wright»
},
«tags»: [
{
«id»: «21c1ac3a−9c1b−
«name»: «JavaScript»
},
{
«id»: «ac9c2f73−
«name»: «TypeScript»
},
{
«id»: «c35defc4−
«name»: «Deno»
},
{
«id»: «d7c2f180−
«name»: «Rust»
}
]
}
]
Получение содержимого публикации
Следующей операцией, которую необходимо реализовать, является GET /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/
{
«id»: «
«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−
«name»: «James Wright»
},
«tags»: [
{
«id»: «21c1ac3a−9c1b−
«name»: «JavaScript»
},
{
«id»: «ac9c2f73−
«name»: «TypeScript»
},
{
«id»: «c35defc4−
«name»: «Deno»
},
{
«id»: «d7c2f180−
«name»: «Rust»
}
]
}
Работа с несуществующими сообщениями
Расширение нашей GET /postsоперации для получения отдельного сообщения по его идентификатору привело к ошибке. Запросим содержимое поста по несуществующему ID:
$ curl -v http: //localhost:8000/posts/
> GET /posts/
> Host: localhost:8000
>
> Accept: */*
>
< HTTP/1.1 200 OK
<
<
<
Поскольку 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/
> GET /posts/
> Host: localhost:8000
>
> Accept: */*
>
< HTTP/1.1 500 Internal Server Error
<
<
<
Post not found with ID
Это, безусловно, улучшение, но, возможно, код состояния не точен. Этот ответ является результатом не ошибки приложения, а того, что пользователь указал отсутствующий пост. В этом случае лучше подойдет 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/
> GET /posts/
> Host: localhost:8000
>
> Accept: */*
>
< HTTP/1.1 404 Not Found
<
<
<
Post not found with ID Post not found with ID
Мы также должны добавить 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 «
«authorId»: «91ef4450−
«title»: «New post»,
«contents»: «This was submitted via our new API endpoint!»,
«tagIds»: [«6a7e1f4d−
}' http: //localhost:8000/posts | jq
{
«id»: «
}
Затем мы можем убедиться, что это было успешно сохранено в нашей базе данных, запросив сообщение по сгенерированному UUID:
$ curl http: //localhost:8000/posts/
{
«id»: «
«title»: «New post»,
«contents»: «This was submitted via our new API endpoint!»,
«author»: {
«id»: «91ef4450−
«name»: «James Wright»
},
«tags»: [
{
«id»: «6a7e1f4d−
«name»: «C#»
},
{
«id»: «f9076c31−
«name»: «Go»
}
]
}
Редактирование существующих сообщений
Чтобы завершить наш сервис, мы собираемся реализовать PATCH /posts/
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 «
«contents»: «This was edited via our new API endpoint!»
}' http: //localhost:8000/posts/
{
«id»: «
}
$ curl http: //localhost:8000/posts/
«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
Резюме
Создание небольших
Тем не менее, более крупные или более сложные сервисы могут выиграть от полной инфраструктуры, такой как Oak. Однако для микросервисов Reno предоставляет очень маленькую ненавязчивую поверхность API, которая позволяет им масштабироваться по мере роста наших