RESTfull API приложение на фреймворке Express.js
01.11.2021
Теги: API • Backend • Express.js • HTTP • JavaScript • Node.js • Web-разработка • БазаДанных • Блог • Сервер • Фреймворк
Сделаем небольшое 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) /* .......... */
Исходные коды можно скачать здесь.
10 марта 2022 года MongoDB предупредила клиентов из РФ и Белоруси, что удалит все их данные на платформе MongoDB Atlas. Чтобы наше приложение работало, установим базу данных локально, для этого скачиваем zip-архив Community Server здесь. Распаковываем архив в директорию C:\mongodb
, создаем директорию для хранения баз данных C:\data\db
и запускаем сервер:
> C:\mongodb\bin\mongod
Теперь осталось только изменить значение переменной DB_URL
в файле index.js
:
const DB_URL = 'mongodb://localhost:27017'
- Магазин на JavaScript, часть 19 из 19. Редактирование характеристик и рефакторинг приложения
- Магазин на JavaScript, часть 18 из 19. Панель управления: редактирование категорий и брендов
- Магазин на JavaScript, часть 17 из 19. Панель управления: список заказов, категорий и брендов
- Магазин на JavaScript, часть 15 из 19. Работа с заказами на сервере, оформление заказа
- Магазин на JavaScript, часть 14 из 19. Кнопка «Назад», страница товара, корзина покупателя
- Магазин на JavaScript, часть 13 из 19. Хранилище каталога, компонент витрины, кнопка «Назад»
- Магазин на JavaScript, часть12 из 19. Запросы на сервер, состояние приложения, Signup и Login
Поиск: API • JavaScript • Web-разработка • База данных • Блог • Backend • Фреймворк • Node.js • Express.js • RESTfull • HTTP