RESTfull API приложение на фреймворке Express.js

01.11.2021

Теги: APIBackendExpress.jsHTTPJavaScriptNode.jsWeb-разработкаБазаДанныхБлогСерверФреймворк

Сделаем небольшое RESTfull API приложение на фреймворке Express.js. Это будет блог, каждый пост содержит заголовок, автора, контент и картинку. Данные будем хранить в базе данных MongoDB. Для тестирования используем расширение REST Client для VS Code. Практической ценности не имеет, просто первое знакомство с фреймворком.

Первые шаги

Итак, создаем пустую директорию, переходим в нее и выполняем команду:

> npm init -y

Устанавливаем фреймфорк Express.js как первую зависимость:

> npm install express

Редактируем секцию scripts файла package.json для запуска приложения:

{
    "name": "backend-restfull-api",
    "version": "1.0.0",
    "description": "RESTfull API example",
    "main": "index.js",
    "type": "module",
    "scripts": {
        "start": "node index.js"
    },
    "keywords": [
        "RESTlull API",
        "JavaScript",
        "Node.js",
        "Express.js"
    ],
    "author": "Tokmakov Evgeniy",
    "license": "ISC",
    "dependencies": {
        "express": "^4.17.1"
    }
}

Теперь запустим простой сервер, для этого создаем файл index.js:

import express from 'express'

const PORT = 5000

const app = express()

app.listen(PORT, () => console.log('Server started ON PORT', PORT))
> npm run start # или просто npm start
Server started ON PORT 5000

CET и POST запросы

Сервер работает, но пока не реагирует запросы от клиентов. Нужно указать серверу, какие endpoint обрабатывать и что делать при поступлении GET, POST, PUT и DELETE запросов.

import express from 'express'

const PORT = 5000

const app = express()

app.get('/', (req, res) => {
    res.status(200).json('Hello, world!');
})

app.listen(PORT, () => console.log('Server started ON PORT', PORT))

Чтобы увидеть изменения, надо перезапустить сервер и открыть браузер по адресу http://localhost:5000/. Перезапускать сервер при каждом изменении в коде неудобно, поэтому устанавливаем пакет nodemon как dev-зависимость.

> npm install nodemon --save-dev

Добавляем в секцию scripts файла package.json новую команду:

{
    ..........
    "scripts": {
        "start": "node index.js",
        "start-dev": "nodemon index.js"
    },
    ..........
}
> npm run start-dev
[nodemon] 2.0.14
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node index.js`
Server started ON PORT 5000

Хорошо, теперь нам нужно расширение REST Client для VS Code для отправки POST-запроса — создаем файл test.http (см. здесь):

GET / HTTP/1.1
Host: localhost:5000

###

POST / HTTP/1.1
Host: localhost:5000
Content-type: application/json; charset=utf-8

{
    "name": "Сергей",
    "age": 30
}

А наш сервер в ответ на POST-запрос будет возвращать тело запроса:

import express from 'express'

const PORT = 5000

const app = express()

// middleware для работы с json
app.use(express.json())

// обрабатываем GET-запрос
app.get('/', (req, res) => {
    res.status(200).json('Hello, world!');
})

// обрабатываем POST-запрос
app.post('/', (req, res) => {
    res.status(200).json(req.body);
})

app.listen(PORT, () => console.log('Server started ON PORT', PORT))

Ответ сервера на POST-запрос будет таким:

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 32
ETag: W/"20-XHK9AP4s7Tt9HcLtg6rcWFT1Ahk"
Date: Mon, 01 Nov 2021 10:38:25 GMT
Connection: close

{
    "name":"Сергей",
    "age":30
}

Работа с базой данных

Нужно зарегистрироваться на сайте mongodb.com, создать проект и новый кластер. Как это сделать, смотрите видео на YouTube здесь. Кроме того, установим пакет mongoose для работы с базой данных.

> npm install mongoose
import express from 'express'
import mongoose from 'mongoose'

const PORT = 5000
const DB_URL = 'mongodb+srv://username:userpass@cluster0.o9nrl.mongodb.net/myFirstDatabase?retryWrites=true&w=majority'

const app = express()

// middleware для работы с json
app.use(express.json())

// обрабатываем GET-запрос
app.get('/', (req, res) => {
    res.status(200).json('Hello, world!');
})

// обрабатываем POST-запрос
app.post('/', (req, res) => {
    res.status(200).json(req.body);
})

async function startApp() {
    try {
        await mongoose.connect(DB_URL)
        app.listen(PORT, () => console.log('Server started ON PORT', PORT))
    } catch(e) {
        console.log(e)
    }
}

startApp()

Модель данных поста

Наше приложение — это блог. Пост блога содержит заголовок, контент, автора и картинку. Создаем модель сущности, это файл Post.js.

import mongoose from 'mongoose'

const Post = new mongoose.Schema({
    title: {type: String, required: true},
    content: {type: String, required: true},
    author: {type: String, required: true},
    picture: {type: String}
})

export default mongoose.model('Post', Post)

Теперь попробуем записать в базу данных новый документ — пост блога. Изменяем код index.js и создаем новый запрос в test.http.

import express from 'express'
import mongoose from 'mongoose'
import Post from './Post.js'

const PORT = 5000
const DB_URL = 'mongodb+srv://username:userpass@cluster0.o9nrl.mongodb.net/myFirstDatabase?retryWrites=true&w=majority'

const app = express()

// middleware для работы с json
app.use(express.json())

// обрабатываем GET-запрос
app.get('/', (req, res) => {
    res.status(200).json('Hello, world!')
})

// обрабатываем POST-запрос
app.post('/', async (req, res) => {
    try {
        const {title, content, author, picture} = req.body
        const post = await Post.create({title, content, author, picture})
        res.json(post)
    } catch(e) {
        res.status(500).json(e)
    }
})

async function startApp() {
    try {
        await mongoose.connect(DB_URL)
        app.listen(PORT, () => console.log('Server started ON PORT', PORT))
    } catch(e) {
        console.log(e)
    }
}

startApp()
GET / HTTP/1.1
Host: localhost:5000

###

POST / HTTP/1.1
Host: localhost:5000
Content-type: application/json; charset=utf-8

{
    "title": "Заголовок первого поста",
    "content": "Контент первого поста",
    "author": "Автор первого поста",
    "picture": ""
}

Ответ сервера в случае успеха:

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 211
ETag: W/"d3-8u1BfZ6yBtW3hPvVj4iL3hiwfC8"
Date: Mon, 01 Nov 2021 11:49:21 GMT
Connection: close

{
    "title":"Заголовок первого поста",
    "content":"Контент первого поста",
    "author":"Автор первого поста",
    "picture":"",
    "_id":"617fd441dec41322ebcbad3c",
    "__v":0
}

Ответ сервера в случае ошибки:

HTTP/1.1 500 Internal Server Error
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 340
ETag: W/"154-eqDE7IyanZc1NyW7AH8CnpedOXg"
Date: Mon, 01 Nov 2021 11:53:37 GMT
Connection: close

{
    "errors": {
        "author": {
            "name":"ValidatorError",
            "message":"Path `author` is required.",
            "properties": {
                "message":"Path `author` is required.",
                "type":"required",
                "path":"author"
            },
            "kind":"required",
            "path":"author"
        }
    },
    "_message":"Post validation failed",
    "name":"ValidationError",
    "message":"Post validation failed: author: Path `author` is required."
}

Маршруты приложения

Описывать все конечные точки в файле index.js — плохая идея. Создадим для этой цели отдельный файл postRouter.js.

import Router from 'express'
import Post from './Post.js'

const postRouter = new Router()

// все посты блога
postRouter.get('/post', async (req, res) => {
    // .....
})

// один пост блога
postRouter.get('/post/:id', async (req, res) => {
    // .....
})

// создание поста блога
postRouter.post('/post', async (req, res) => {
    try {
        const {title, content, author, picture} = req.body
        const post = await Post.create({title, content, author, picture})
        res.json(post)
    } catch(e) {
        res.status(500).json(e)
    }
})

// обновление поста блога
postRouter.put('/post', async (req, res) => {
    // .....
})

// удаление поста блога
postRouter.delete('/post/:id', async (req, res) => {
    // .....
})

export default postRouter
import express from 'express'
import mongoose from 'mongoose'
import postRouter from './postRouter.js'

const PORT = 5000
const DB_URL = 'mongodb+srv://username:userpass@cluster0.o9nrl.mongodb.net/myFirstDatabase?retryWrites=true&w=majority'

const app = express()

// middleware для работы с json
app.use(express.json())
// маршруты для работы с постами
app.use('/api', postRouter)

async function startApp() {
    try {
        await mongoose.connect(DB_URL)
        app.listen(PORT, () => console.log('Server started ON PORT', PORT))
    } catch(e) {
        console.log(e)
    }
}

startApp()

По аналогии с созданием маршрутов для постов блога, мы могли бы создать маршруты для работы с пользователями:

// маршруты для работы с юзерами
app.use('/api', userRouter)

У нас пока работает только один маршрут — создание поста блога. Давайте проверим, что он работает и добавляет в БД новый пост.

POST /api/post HTTP/1.1
Host: localhost:5000
Content-type: application/json; charset=utf-8

{
    "title": "Заголовок второго поста",
    "content": "Контент второго поста",
    "author": "Автор второго поста",
    "picture": ""
}
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 211
ETag: W/"d3-A5LtHlp1mEzd25I7M+2dH2NzOUs"
Date: Mon, 01 Nov 2021 12:36:27 GMT
Connection: close

{
    "title":"Заголовок второго поста",
    "content":"Контент второго поста",
    "author":"Автор второго поста",
    "picture":"",
    "_id":"617fdf4b58798057b2baef37",
    "__v":0
}

Контроллер приложения

Создаем контроллер PostController.js, который будет обрабатывать HTTP-запросы — получать, создавать, обновлять и удалять посты блога.

import Post from './Post.js'

class PostController {
    async getAll(req, res) {
        // .....
    }

    async getOne(req, res) {
        // .....
    }

    async create(req, res) {
        try {
            const {title, content, author, picture} = req.body
            const post = await Post.create({title, content, author, picture})
            res.json(post)
        } catch(e) {
            res.status(500).json(e)
        }
    }


    async update(req, res) {
        // .....
    }

    async delete(req, res) {
        // .....
    }
}

export default new PostController();

И внесем изменения в postRouter.js:

import Router from 'express'
import PostController from './PostController.js'

const postRouter = new Router()

// все посты блога
postRouter.get('/post', PostController.getAll)
// один пост блога
postRouter.get('/post/:id', PostController.getOne)
// создание поста блога
postRouter.post('/post', PostController.create)
// обновление поста блога
postRouter.put('/post', PostController.update)
// удаление поста блога
postRouter.delete('/post/:id', PostController.delete)

export default postRouter;

Осталось только реализовать все методы класса PostController:

import Post from './Post.js'

class PostController {
    async getAll(req, res) {
        try {
            const posts = await Post.find();
            res.json(posts)
        } catch(e) {
            res.status(500).json(e)
        }
    }

    async getOne(req, res) {
        try {
            if (!req.params.id) {
                res.status(400).json({message: 'Не указан id поста'})
                return;
            }
            const post = await Post.findById(req.params.id)
            if (post) {
                res.json(post)
            } else {
                res.status(400).json({message: 'Пост блога не найден'})
            }
        } catch(e) {
            res.status(500).json(e)
        }
    }

    async create(req, res) {
        try {
            const {title, content, author, picture} = req.body
            const post = await Post.create({title, content, author, picture})
            res.json(post)
        } catch(e) {
            res.status(500).json(e)
        }
    }

    async update(req, res) {
        try {
            const post = req.body;
            if (!post._id) {
                res.status(400).json({message: 'Не указан id поста'})
                return
            }
            const updated = await Post.findByIdAndUpdate(post._id, post, {new: true})
            if (updated) {
                res.json(updated)
            } else {
                res.status(400).json({message: 'Пост блога не найден'})
            }
        } catch(e) {
            res.status(500).json(e)
        }
    }

    async delete(req, res) {
        try {
            if (!req.params.id) {
                res.status(400).json({message: 'Не указан id поста'})
                return;
            }
            const deleted = await Post.findByIdAndRemove(req.params.id)
            if (deleted) {
                res.json(deleted)
            } else {
                res.status(400).json({message: 'Пост блога не найден'})
            }
        } catch(e) {
            res.status(500).json(e)
        }
    }
}

export default new PostController()

Теперь проверим работу всех методов, для этого редактируем файл test.http:

### получение списка всех постов
GET /api/post HTTP/1.1
Host: localhost:5000

### получение одного поста блога
GET /api/post/61825d44896759a8d9b350c5 HTTP/1.1
Host: localhost:5000

### добавление нового поста блога
POST /api/post HTTP/1.1
Host: localhost:5000
Content-type: application/json; charset=utf-8

{
    "title": "Заголовок третьего поста",
    "content": "Контент третьего поста",
    "author": "Автор третьего поста",
    "picture": ""
}

### обновление одного поста блога
PUT /api/post HTTP/1.1
Host: localhost:5000
Content-type: application/json; charset=utf-8

{
    "title": "Заголовок третьего поста (обновление)",
    "content": "Контент третьего поста (обновление)",
    "author": "Автор третьего поста (обновление)",
    "picture": "",
    "_id": "61825d44896759a8d9b350c5"
}

### удаление одного поста блога
DELETE /api/post/61825d44896759a8d9b350c5 HTTP/1.1
Host: localhost:5000

Модель приложения

Сейчас бизнес-логику у нас реализует контроллер, что не есть хорошо — это работа модели в шаблоне MVC (Model-View-Controller), давайте это исправим. Создаем файл PostService.js — контроллер будет обращаться к методам модели, а модель — взаимодейстровать с базой данных через mongoose.

import Post from './Post.js'

class PostService {
    async getAll() {
        const posts = await Post.find()
        return posts
    }

    async getOne(id) {
        if (!id) {
            throw new Error('Не указан id поста')
        }
        const post = await Post.findById(id)
        if (!post) {
            throw new Error('Пост блога не найден')
        }
        return post
    }

    async create(data) {
        const {title, content, author, picture} = data
        const created = await Post.create({title, content, author, picture})
        return created
    }

    async update(data) {
        if (!data._id) {
            throw new Error('Не указан id поста')
        }
        const updated = await Post.findByIdAndUpdate(data._id, data, {new: true})
        if (!updated) {
            throw new Error('Пост блога не найден')
        }
        return updated
    }

    async delete(id) {
        if (!id) {
            throw new Error('Не указан id поста')
        }
        const deleted = await Post.findByIdAndRemove(id)
        if (!deleted) {
            throw new Error('Пост блога не найден')
        }
        return deleted
    }
}

export default new PostService()

И уберем все лишнее из контроллера:

import PostService from './PostService.js';

class PostController {
    async getAll(req, res) {
        try {
            const posts = await PostService.getAll();
            res.json(posts);
        } catch (e) {
            res.status(500).json({error: true, message: e.message})
        }
    }

    async getOne(req, res) {
        try {
            const post = await PostService.getOne(req.params.id)
            res.json(post)
        } catch (e) {
            res.status(500).json({error: true, message: e.message})
        }
    }

    async create(req, res) {
        try {
            const created = await PostService.create(req.body)
            res.json(created)
        } catch (e) {
            res.status(500).json({error: true, message: e.message})
        }
    }

    async update(req, res) {
        try {
            const updated = await PostService.update(req.body)
            res.json(updated);
        } catch (e) {
            res.status(500).json({error: true, message: e.message})
        }
    }

    async delete(req, res) {
        try {
            const deleted = await PostService.delete(req.params.id)
            res.json(deleted)
        } catch (e) {
            res.status(500).json({error: true, message: e.message})
        }
    }
}

export default new PostController()

Загрузка изображения

Для загрузки изображения нам портебуется пакет express-fileupload — устанавливаем, импортируем и добавляем middleware:

> npm install express-fileupload
import express from 'express'
import mongoose from 'mongoose'
import postRouter from './postRouter.js'
import fileUpload from 'express-fileupload'

const PORT = 5000
const DB_URL = '.....'

const app = express()

// middleware для работы с json
app.use(express.json())
// middleware для загрузки файлов
app.use(fileUpload())
// маршруты для работы с постами
app.use('/api', postRouter)

async function startApp() {
    try {
        await mongoose.connect(DB_URL)
        app.listen(PORT, () => console.log('Server started ON PORT', PORT))
    } catch(e) {
        console.log(e)
    }
}

startApp()

Для тестирования отправки данных редактируем test.http:

### создание поста с изображением
POST /api/post HTTP/1.1
Host: localhost:5000
Content-Type: multipart/form-data; boundary=MultiPartFormDataBoundary

--MultiPartFormDataBoundary
Content-Disposition: form-data; name="title"

Заголовок четвертого поста
--MultiPartFormDataBoundary
Content-Disposition: form-data; name="content"

Контент четвертого поста
--MultiPartFormDataBoundary
Content-Disposition: form-data; name="author"

Автор четвертого поста
--MultiPartFormDataBoundary
Content-Disposition: form-data; name="picture"; filename="picture.png"
Content-Type: image/png

< ./picture.png
--MultiPartFormDataBoundary--

Добавим в контроллер вывод информации о полях POST-запроса:

import PostService from './PostService.js'

class PostController {
    /* .......... */
    async create(req, res) {
        try {
            console.log(req.body)
            console.log(req.files)
            const created = await PostService.create(req.body)
            res.json(created)
        } catch (e) {
            res.status(500).json({error: true, message: e.message})
        }
    }
    /* .......... */
}

export default new PostController()

Отправляем POST-запрос, но перед этим положим в директорию проекта файл picture.png:

> npm run start-dev
[nodemon] 2.0.14
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node index.js`

Server started ON PORT 5000

{
  title: 'Заголовок четвертого поста',
  content: 'Контент четвертого поста',
  author: 'Автор четвертого поста'
}
{
  picture: {
    name: 'picture.png',
    data: <Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 00 78 00 00 00 78 ... 307 more bytes>,
    size: 357,
    encoding: '7bit',
    tempFilePath: '',
    truncated: false,
    mimetype: 'image/png',
    md5: 'c6569fbff08e0cde33c2e25cff053cc4',
    mv: [Function: mv]
  }
}

Хорошо, теперь реализуем сохранение файла изображения:

import PostService from './PostService.js'

class PostController {
    /* .......... */
    async create(req, res) {
        try {
            const created = await PostService.create(req.body, req.files?.picture)
            res.json(created)
        } catch (e) {
            res.status(500).json({error: true, message: e.message})
        }
    }
    /* .......... */
}

export default new PostController()
import Post from './Post.js'
import FileService from './FileService.js'

class PostService {
    /* .......... */
    async create(data, image) {
        const picture = FileService.save(image)
        const {title, content, author} = data
        const created = await Post.create({title, content, author, picture})
        return created
    }
    /* .......... */
}

export default new PostService()

Для сохранения файла на диск создадим отдельный класс:

import * as uuid from 'uuid'
import * as path from 'path'

class FileService {
    save(file) {
        if (!file) return null
        try {
            const [, ext] = file.mimetype.split('/')
            const fileName = uuid.v4() + '.' + ext
            const filePath = path.resolve('static', fileName)
            file.mv(filePath)
            return fileName
        } catch (e) {
            console.log(e)
        }
    }
}

export default new FileService()

Чтобы у загруженных файлов были уникальные имена — устанавливаем пакет uuid:

> npm install uuid

Осталось только создать директорию static внутри проекта. Чтобы изображение можно было увидеть в браузере, добавляем middleware:

/* .......... */
// middleware для работы с json
app.use(express.json())
// middleware для загрузки файлов
app.use(fileUpload())
// middleware для статики (img, css)
app.use(express.static('static'))
// маршруты для работы с постами
app.use('/api', postRouter)
/* .......... */

Поиск: API • JavaScript • Web-разработка • База данных • Блог • Backend • Фреймворк • Node.js • Express.js • RESTfull • HTTP

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.