Когда вы разрабатываете 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.