Як створити надзвичайно швидкі REST API за допомогою Node.js, MongoDB, Fastify та Swagger

Імовірно, жоден веб-розробник не чужий для REST API та проблем, які створює ефективне та дієве рішення API .

Ці проблеми включають:

  • Швидкість (час відгуку API)
  • Документація (Очистити стислі документи, описуючи API)
  • Архітектура та стійкість (підтримка та розширення кодової бази)

У цьому посібнику ми збираємося розглянути все вищезазначене, використовуючи комбінацію Node.js , MongoDB , Fastify та Swagger .

Вихідний код проекту доступний на GitHub.

Перш ніж ми почнемо ...

Ви повинні володіти деякими початковими / середніми знаннями JavaScript , чути про Node.js та MongoDB, і знати, що таке REST API .

Нижче наведено декілька посилань для отримання оновлення:

  • JavaScript
  • Node.js
  • MongoDB
  • REST API

Технологія, якою ми будемо користуватися:

  • Закріпити
  • Мангуст
  • Своггер

Рекомендується відкрити вищезазначені сторінки в нових вкладках для зручності.

Вам потрібно буде встановити наступне:

  • NodeJS / NPM
  • MongoDB
  • Листоноша

Вам також знадобляться IDE і термінал, я використовую iTerm2 для Mac і Hyper для Windows.

Давайте розпочнемо!

Ініціюйте новий проект, відкривши термінал, виконуючи кожен з наступних рядків коду:

mkdir fastify-api cd fastify-api mkdir src cd src touch index.js npm init

У наведеному вище коді ми створили два нові каталоги, перейшли до них, створили index.jsфайл та парафірували наш проект через npm.

Вам буде запропоновано ввести кілька значень під час ініціалізації нового проекту, які ви можете залишити пустими та оновити пізніше.

Після завершення в каталозі генерується файл package.json src. У цьому файлі ви можете змінити значення, введені під час ініціалізації проекту.

Далі ми встановлюємо всі необхідні нам залежності :

npm i nodemon mongoose fastify fastify-swagger boom

Нижче наводиться короткий опис того, що робить кожен пакет, цитований на відповідних веб-сайтах:

нодемон

nodemon - це інструмент, який допомагає розробляти додатки на основі node.js шляхом автоматичного перезапуску програми вузла, коли виявляються зміни файлів у каталозі.

nodemon не вимагає жодних додаткових змін у вашому коді або способі розробки. nodemon - це обгортка для заміни node, для використання nodemonслова replace nodeу командному рядку під час виконання вашого сценарію.

Щоб встановити nodemon , нам потрібно додати наступний рядок коду до нашого package.jsonфайлу в об'єкті скриптів:

“start”: “./node_modules/nodemon/bin/nodemon.js ./src/index.js”,

package.jsonТепер наш файл повинен виглядати наступним чином:

{ "name": "fastify-api", "version": "1.0.0", "description": "A blazing fast REST APIs with Node.js, MongoDB, Fastify and Swagger.", "main": "index.js", "scripts": { "start": "./node_modules/nodemon/bin/nodemon.js ./src/index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Siegfried Grimbeek  (www.siegfriedgrimbeek.co.za)", "license": "ISC", "dependencies": { "boom": "^7.2.2", "fastify": "^1.13.0", "fastify-swagger": "^0.15.3", "mongoose": "^5.3.14", "nodemon": "^1.18.7" } }

мангуст

Mongoose пропонує пряме рішення на основі схем для моделювання даних вашого додатка. Він включає вбудований тип лиття, перевірку, побудову запитів, гачки бізнес-логіки та багато іншого, нестандартно.

закріпити

Fastify - це веб-фреймворк, зосереджений на забезпеченні найкращого досвіду для розробників з найменшими витратами та потужною архітектурою плагінів. Він натхненний Hapi та Express, і, наскільки нам відомо, це одна з найшвидших веб-платформ у місті.

фастифікувати

Генератор документації Swagger для Fastify. Він використовує схеми, які ви декларуєте у своїх маршрутах, для створення документа, сумісного з хитрощами.

бум

boom надає набір утиліт для повернення помилок HTTP.

Налаштуйте сервер і створіть перший маршрут!

Додайте наступний код у свій index.jsфайл:

// Require the framework and instantiate it const fastify = require('fastify')({ logger: true }) // Declare a route fastify.get('/', async (request, reply) => { return { hello: 'world' } }) // Run the server! const start = async () => { try { await fastify.listen(3000) fastify.log.info(`server listening on ${fastify.server.address().port}`) } catch (err) { fastify.log.error(err) process.exit(1) } } start()

Нам потрібен фреймворк Fastify , оголошений наш перший маршрут та ініціалізація сервера port 3000, код досить зрозумілий, але зверніть увагу на об’єкт параметрів, переданий при ініціалізації Fastify :

// Require the fastify framework and instantiate it const fastify = require('fastify')({ logger: true })

The above code enables Fastify’s built in logger which is disabled by default.

You can now run the follow code in your src directory in your terminal:

npm start

Now when you navigate to //localhost:3000/ you should see the {hello:world} object returned.

We will get back to the index.js file but for now let’s move on to setting up our database.

Start MongoDB and create the model!

Once MongoDB has been successfully installed, you can open a new terminal window and start up a MongoDB instance by running the following:

mongod

With MongoDB, we do not need to create a database. We can just specify a name in the setup and as soon as we store data, MongoDB will create this database for us.

Add the following to your index.js file:

... // Require external modules const mongoose = require('mongoose') // Connect to DB mongoose.connect(‘mongodb://localhost/mycargarage’) .then(() => console.log(‘MongoDB connected…’)) .catch(err => console.log(err)) ...

In the above code we require Mongoose and connect to our MongoDB database. The database is called mycargarage and if all went well, you will now see MongoDB connected... in your terminal.

Notice that you did not have to restart the app, thanks to the Nodemon package that we added earlier.

Now that our database is up and running, we can create our first Model. Create a new folder within the src directory called models, and within it create a new file called Car.js and add the following code:

// External Dependancies const mongoose = require('mongoose') const carSchema = new mongoose.Schema({ title: String, brand: String, price: String, age: Number, services: { type: Map, of: String } }) module.exports = mongoose.model('Car', carSchema)

The above code declares our carSchema that contains all the information related to our cars. Apart from the two obvious data types: String and Number. We also make use of a Map which is relatively new to Mongoose and you can read more about it here. We then export our carSchema to be used within our app.

We could proceed with setting up our routes, controllers and config in the index.js file, but part of this tutorial is demonstrating a sustainable codebase. Therefore each component will have its own folder.

Create the car controller

To get started with creating the controllers, we create a folder in the src directory called controllers, and within the folder, we create a carController.js file:

// External Dependancies const boom = require('boom') // Get Data Models const Car = require('../models/Car') // Get all cars exports.getCars = async (req, reply) => { try { const cars = await Car.find() return cars } catch (err) { throw boom.boomify(err) } } // Get single car by ID exports.getSingleCar = async (req, reply) => { try { const id = req.params.id const car = await Car.findById(id) return car } catch (err) { throw boom.boomify(err) } } // Add a new car exports.addCar = async (req, reply) => { try { const car = new Car(req.body) return car.save() } catch (err) { throw boom.boomify(err) } } // Update an existing car exports.updateCar = async (req, reply) => { try { const id = req.params.id const car = req.body const { ...updateData } = car const update = await Car.findByIdAndUpdate(id, updateData, { new: true }) return update } catch (err) { throw boom.boomify(err) } } // Delete a car exports.deleteCar = async (req, reply) => { try { const id = req.params.id const car = await Car.findByIdAndRemove(id) return car } catch (err) { throw boom.boomify(err) } }

The above may seem like a little much to take in, but it is actually really simple.

  • We require boom to handle our errors: boom.boomify(err).
  • We export each of our functions which we will use in our route.
  • Each function is an async function that can contain an await expression that pauses the execution of the async function and waits for the passed Promise’s resolution, and then resumes the async function’s execution and returns the resolved value. Learn more here.
  • Each function is wrapped in a try / catch statement. Learn more here.
  • Each function takes two parameters: req (the request) and reply (the reply). In our tutorial we only make use of the request parameter. We will use it to access the request body and the request parameters, allowing us to process the data. Learn more here.
  • Take note of the code on line 31:

    const car = new Car({ …req.body })

    This makes use of the JavaScript spread operator. Learn more here.

  • Take note of the code on line 42:

    const { …updateData } = car

    This makes use of the JavaScript destructuring in conjunction with the spread operator. Learn more here.

Other than that, we make use of some standard Mongoose features used to manipulate our database.

You are probably burning to fire up your API and do a sanity check, but before we do this, we just need to connect the controller to the routes and then lastly connect the routes to the app.

Create and import the routes

Once again, we can start by creating a folder in the root directory of our project, but this time, it is called routes. Within the folder, we create an index.js file with the following code:

// Import our Controllers const carController = require('../controllers/carController') const routes = [ { method: 'GET', url: '/api/cars', handler: carController.getCars }, { method: 'GET', url: '/api/cars/:id', handler: carController.getSingleCar }, { method: 'POST', url: '/api/cars', handler: carController.addCar, schema: documentation.addCarSchema }, { method: 'PUT', url: '/api/cars/:id', handler: carController.updateCar }, { method: 'DELETE', url: '/api/cars/:id', handler: carController.deleteCar } ] module.exports = routes

Here we are requiring our controller and assigning each of the functions that we created in our controller to our routes.

As you can see, each route consists out of a method, a url and a handler, instructing the app on which function to use when one of the routes is accessed.

The :id following some of the routes is a common way to pass parameters to the routes, and this will allow us to access the id as follows:

//127.0.0.1:3000/api/cars/5bfe30b46fe410e1cfff2323

Putting it all together and testing our API

Now that we have most of our parts constructed, we just need to connect them all together to start serving data via our API. Firstly we need to import our routes that we created by adding the following line of code to our main index.js file:

const routes = require(‘./routes’)

We then need to loop over our routes array to initialise them with Fastify. We can do this with the following code, which also needs to be added to the main index.js file:

routes.forEach((route, index) => { fastify.route(route) })

Now we are ready to start testing!

The best tool for the job is Postman, which we will use to test all of our routes. We will be sending our data as raw objects in the body of the request and as parameters.

Finding all cars:

Finding a single car:

Adding a new car**:

** The services appear to be empty, but the information does in fact persist to the database.

Updating a car:

Deleting a car:

We now have a fully functional API — but what about the documentation? This is where Swagger is really handy.

Adding Swagger and wrapping up.

Now we will create our final folder called config. Inside we will create a file called swagger.js with the following code:

exports.options = { routePrefix: '/documentation', exposeRoute: true, swagger: { info: { title: 'Fastify API', description: 'Building a blazing fast REST API with Node.js, MongoDB, Fastify and Swagger', version: '1.0.0' }, externalDocs: { url: '//swagger.io', description: 'Find more info here' }, host: 'localhost', schemes: ['http'], consumes: ['application/json'], produces: ['application/json'] } }

The above code is an object with the options which we will pass into our fastify-swagger plugin. To do this, we need to add the following to our index.js file:

// Import Swagger Options const swagger = require(‘./config/swagger’) // Register Swagger fastify.register(require(‘fastify-swagger’), swagger.options)

And then we need to add the following line after we have initialised our Fastify server:

... await fastify.listen(3000) fastify.swagger() fastify.log.info(`listening on ${fastify.server.address().port}`) ...

And that is it! If you now navigate to //localhost:3000/documentation, you should see the following:

As simple as that! You now have self updating API documentation that will evolve with your API. You can easily add additional information to your routes, see more here.

Whats Next?

Now that we have a basic API in place, the possibilities are limitless. It can be used as the base for any app imaginable.

In the next tutorial, we will integrate GraphQL and eventually integrate the frontend with Vue.js too!