Когда вы разрабатываете SPA (одностраничное приложение), большую часть времени вам нужно обращаться к API. С помощью файла JSON, обещаний, встроенных функций JavaScript и, конечно же, Express, я покажу вам, как подготовить базовый API RESTful.
Подготовить сервер
Инициализировал проект
Перед этим убедитесь, что у вас установлены как Node.js, так и NPM.
node -v && npm -v
Затем создайте новый проект с помощью команды npm.
npm init -y
И установите такие пакеты, как express, morgan и nodemon. 📦📦📦
$ npm install express $ npm install --save-dev morgan nodemon
Первый маршрут
Создайте новый файл index.js.
touch index.js
Скопируйте и вставьте этот код, чтобы создать простой сервер, работающий на порту 1337.
// Import packages
const express = require('express')
const morgan = require('morgan')
// App
const app = express()
// Morgan
app.use(morgan('tiny'))
// First route
app.get('/', (req, res) => {
res.json({ message: 'Hello world' })
})
// Starting server
app.listen('1337')
Запустить сервер с Nodemon
Откройте package.json в своем редакторе и добавьте строку в объект скрипта перед словом «test».
"dev": "node_modules/.bin/nodemon -e js",
Если вы используете Node версии ≤ 8.2.1, вам нужно будет добавить флаг «- harmony » для использования оператора распространения.
"dev": "node_modules/.bin/nodemon --harmony -e js",
Сохраните изменения и запустите команду в своем терминале.
npm run dev
Откройте навигатор и перейдите по адресу http: // localhost: 1337 или с помощью команды CURL.
curl -i http://localhost:1337
Вернет этот результат.
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 25
Connection: keep-alive
{"message":"Hello world"}
Замечательно! У нас есть объект с именем «message» со значением «Hello world».
Наш API-контент
CRUD операции
Для каждой операции нам понадобятся нативные функции JavaScript.
- Создать: array.push ()
- Чтение: массив
- Прочтите: array.find () (вернуть объект)
- Обновление: array.find (), array.findIndex () (вернуть индекс)
- Удалить: array.find (), array.filter ()
Для Создать, Обновить и Удалить массив данных будет обновлен в файле JSON.
Файловая организация
Добавление некоторых папок и файлов, таких как помощник, модели, маршруты и данные.
└── helpers
├── helper.js
└── middlewares.js
└── data
└──posts.json
└── models
└── post.model.js
└── routes
├── index.routes.js
└── post.routes.js
Вы можете создавать эти папки и файлы с помощью этой команды.
mkdir helpers && cd helpers && touch helper.js && touch middlewares.js && cd ../ && mkdir models && cd models && touch post.model.js && cd ../ && mkdir routes && cd routes && touch index.routes.js && touch post.routes.js && cd ../ && mkdir data && cd data && touch posts.json && cd ../
Схема
Давайте создадим сообщение в блоге (очень оригинальное…) с этими полями подряд.
- id: Число (уникальный и увеличивающийся)
- created_at: Дата (ISO 8601)
- updated_at: Дата (ISO 8601)
- title: Строка
- содержание: Строка
- теги: массив
Данные
Пример данных в posts.json.
[
{
"id": 1,
"title": "First post",
"content": "Lorem Ipsum",
"tags": ["tag1", "tag2", "tag3"],
"createdAt": "Mon Aug 27 2018 15:16:17 GMT+0200 (CEST)",
"updatedAt": "Mon Aug 27 2018 15:16:17 GMT+0200 (CEST)"
},
{
"id": 2,
"title": "Second post",
"content": "Lorem Ipsum again",
"tags": ["tag2", "tag4"],
"createdAt": "Mon Aug 27 2018 16:17:18 GMT+0200 (CEST)",
"updatedAt": "Mon Aug 27 2018 16:17:18 GMT+0200 (CEST)"
}
]
Помощник
Откройте файл helper.js.
const fs = require('fs')
const getNewId = (array) => {
if (array.length > 0) {
return array[array.length - 1].id + 1
} else {
return 1
}
}
const newDate = () => new Date().toString()
function mustBeInArray(array, id) {
return new Promise((resolve, reject) => {
const row = array.find(r => r.id == id)
if (!row) {
reject({
message: 'ID is not good',
status: 404
})
}
resolve(row)
})
}
function writeJSONFile(filename, content) {
fs.writeFileSync(filename, JSON.stringify(content), 'utf8', (err) => {
if (err) {
console.log(err)
}
})
}
module.exports = {
getNewId,
newDate,
mustBeInArray,
writeJSONFile
}
У нас есть 4 функции, очень полезные для моделей.
- getNewId: поиск в массиве последнего идентификатора и приращения 1 для возврата нового идентификатора.
- newDate: возвращает дату вашего сервера в ISO 8601.
- mustBeInArray: вернуть обещание. Используется, когда нам нужно проверить, существует ли строка по идентификатору (Прочитать, Обновить и Удалить).
- writeJSONFile: записать новый массив в данные файла JSON.
Промежуточное ПО
Откройте файл middlewares.js.
function mustBeInteger(req, res, next) {
const id = req.params.id
if (!Number.isInteger(parseInt(id))) {
res.status(400).json({ message: 'ID must be an integer' })
} else {
next()
}
}
function checkFieldsPost(req, res, next) {
const { title, content, tags } = req.body
if (title && content && tags) {
next()
} else {
res.status(400).json({ message: 'fields are not good' })
}
}
module.exports = {
mustBeInteger,
checkFieldsPost
}
У нас есть 2 функции, очень полезные для маршрутов.
- mustBeInteger: перед продолжением проверьте, является ли идентификатор целым числом. Используется, когда нам нужно получить идентификатор (Прочитать, Обновить и Удалить).
- checkFieldsPost: проверьте, прежде чем продолжить, если data. Используется, когда нам нужно получить идентификатор (Создать и Обновить).
Модель
Откройте файл post.model.js.
const filename = '../data/posts.json'
let posts = require(filename)
const helper = require('../helper.js')
function getPosts() {}
function getPost(id) {}
function insertPost(newPost) {}
function updatePost(id, newPost) {}
function deletePost(id) {}
module.exports = {
insertPost,
getPosts,
getPost,
updatePost,
deletePost
}
Мы загружаем файл данных JSON и вспомогательный файл. Затем готовим 4 функции.
- getPosts
- getPost
- insertPost
- updatePost
- deletePost
Все эти функции возвращают обещание. И не забываем экспортировать их в конец файла.
getPosts
Нам просто нужно вернуть массив данных объектов, если массив существует.
function getPosts() {
return new Promise((resolve, reject) => {
if (posts.length === 0) {
reject({
message: 'no posts available',
status: 202
})
}
resolve(posts)
})
}
Возвращаем обещание.
- Если сообщений нет, отобразить настраиваемое сообщение (отклонить, 202 ⚠️).
- Если сообщения, показывать массив сообщений (решить, 200 👌).
getPost
Как и в предыдущей функции, за исключением того, что мы хотим вернуть объект вместо массива. Для этого мы будем использовать встроенную функцию JavaScript find () для получения объекта по идентификатору в параметре функции.
function getPost(id) {
return new Promise((resolve, reject) => {
helper.mustBeInArray(posts, id)
.then(post => resolve(post))
.catch(err => reject(err))
})
}
Возвращаем обещание.
- Если не опубликовать сообщение с этим идентификатором, отобразить сообщение об ошибке (отклонить, 404 ⚠️).
- Если опубликовано, отобразить массив сообщений (решить, 200 👌).
insertPost
Мы вставим новую строку.
function insertPost(newPost) {
return new Promise((resolve, reject) => {
const id = { id: helper.getNewId(posts) }
const date = {
createdAt: helper.newDate(),
updatedAt: helper.newDate()
}
newPost = { ...id, ...date, ...newPost }
posts.push(newPost)
helper.writeJSONFile(filename, posts)
resolve(newPost)
})
}
Возвращаем обещание.
- Если опубликовано, отобразить массив сообщений (решить, 200 👌).
updatePost
Мы будем использовать встроенную функцию JavaScript find () для получения объекта по идентификатору в параметре функции. Как и при добавлении поста, у нас есть некоторый контент от клиента. Затем мы находим индекс строки с помощью встроенной функции findIndex. В этой строке мы добавляем идентификатор, дату обновления и контент.
function updatePost(id, newPost) {
return new Promise((resolve, reject) => {
helper.mustBeInArray(posts, id)
.then(post => {
const index = posts.findIndex(p => p.id == post.id)
id = { id: post.id }
const date = {
createdAt: post.createdAt,
updatedAt: helper.newDate()
}
posts[index] = { ...id, ...date, ...newPost }
helper.writeJSONFile(filename, posts)
resolve(posts[index])
})
.catch(err => reject(err))
})
}
Возвращаем обещание.
- Если сообщений с этим идентификатором нет, отобразить сообщение об ошибке (отклонить, 404 ⚠️).
- Если опубликовано, отобразить массив сообщений (решить, 200 👌).
deletePost
Для этого мы будем использовать встроенную функцию JavaScript find () для получения объекта по идентификатору в параметре функции. После проверки, является ли id целым числом и существует ли строка в массиве, мы удаляем с помощью встроенной функции filter ().
function deletePost(id) {
return new Promise((resolve, reject) => {
helper.mustBeInArray(posts, id)
.then(() => {
posts = posts.filter(p => p.id !== id)
helper.writeJSONFile(filename, posts)
resolve()
})
.catch(err => reject(err))
})
}
Возвращаем обещание.
- Если не опубликовать сообщение с этим идентификатором, отобразить сообщение об ошибке (отклонить, 404 ⚠️).
- Если отправлено сообщение, укажите массив сообщений (решить, 200 👌)
Маршруты
Откройте файл index.routes.js для объявления маршрута «/ api / v1 / posts».
const express = require('express')
const router = express.Router()
module.exports = router
router.use('/api/v1/posts', require('./post.routes'))
Затем в корневом файле index.js для загрузки предыдущего файла.
const app = express()
app.use(morgan('tiny'))
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use(require('./routes/index.routes'))
Теперь мы можем работать с файлом post.routes.js.
const express = require('express')
const router = express.Router()
const post = require('../models/post.model')
const m = require('../helpers/middlewares')
// Routes
module.exports = router
Все сообщения
/* All posts */
router.get('/', async (req, res) => {
await post.getPosts()
.then(posts => res.json(posts))
.catch(err => {
if (err.status) {
res.status(err.status).json({ message: err.message })
} else {
res.status(500).json({ message: err.message })
}
})
})
CURL запрос (200)
curl -i -X GET http://localhost:1337/api/v1/posts
Сообщение по id
/* A post by id */
router.get('/:id', m.mustBeInteger, async (req, res) => {
const id = req.params.id
await post.getPost(id)
.then(post => res.json(post))
.catch(err => {
if (err.status) {
res.status(err.status).json({ message: err.message })
} else {
res.status(500).json({ message: err.message })
}
})
})
Запрос CURL (200, 404, 400).
curl -i -X GET http://localhost:1337/api/v1/posts/1 curl -i -X GET http://localhost:1337/api/v1/posts/404 curl -i -X GET http://localhost:1337/api/v1/posts/string
Вставить новый пост
/* Insert a new post */
router.post('/', m.checkFieldsPost, async (req, res) => {
await post.insertPost(req.body)
.then(post => res.status(201).json({
message: `The post #${post.id} has been created`,
content: post
}))
.catch(err => res.status(500).json({ message: err.message }))
})
Запросы CURL (201, 400).
curl -i -X POST \
-H "Content-Type: application/json" \
-d '{ "title": "test again", "content": "Lorem Ipsum", "tags": ["tag1", "tag4"] }' \
http://localhost:1337/api/v1/posts
curl -i -X POST \
http://localhost:1337/api/v1/posts
Обновить сообщение
/* Update a post */
router.put('/:id', m.mustBeInteger, m.checkFieldsPost, async (req, res) => {
const id = req.params.id
await post.updatePost(id, req.body)
.then(post => res.json({
message: `The post #${id} has been updated`,
content: post
}))
.catch(err => {
if (err.status) {
res.status(err.status).json({ message: err.message })
}
res.status(500).json({ message: err.message })
})
})
Запросы CURL (200, 404, 400, 400).
curl -i -X PUT \
-H "Content-Type: application/json" \
-d '{ "title": "The first", "content": "Lorem Ipsum 2", "tags": ["tag1", "tag4"] }' \
http://localhost:1337/api/v1/posts/1
curl -i -X PUT \
-H "Content-Type: application/json" \
-d '{ "title": "The first", "content": "Lorem Ipsum 2", "tags": ["tag1", "tag4"] }' \
http://localhost:1337/api/v1/posts/404
curl -i -X PUT http://localhost:1337/api/v1/posts/1
curl -i -X PUT http://localhost:1337/api/v1/posts/string
Удалить сообщение
/* Delete a post */
router.delete('/:id', m.mustBeInteger, async (req, res) => {
const id = req.params.id
await post.deletePost(id)
.then(post => res.json({
message: `The post #${id} has been deleted`
}))
.catch(err => {
if (err.status) {
res.status(err.status).json({ message: err.message })
}
res.status(500).json({ message: err.message })
})
})
Запросы CURL (200, 404, 400).
curl -i -X DELETE http://localhost:1337/api/v1/posts/1 curl -i -X DELETE http://localhost:1337/api/v1/posts/404 curl -i -X DELETE http://localhost:1337/api/v1/posts/string
Заключение
Поиздевавшись с Promises, создайте собственный API RESTfull без ограничений базы данных. В нашем случае, если мы хотим добавить авторов. Как завязать отношения? С реальной базой данных это проще. Вы можете легко переключиться на базу данных, изменив файл модели.
Для лучшего API вы можете использовать пакет Joi для проверки схемы вашей модели и использовать это промежуточное программное обеспечение для включения CORS.
И да, этот API доступен на Github.