Πώς να δημιουργήσετε απίστευτα γρήγορα REST API με Node.js, MongoDB, Fastify και Swagger

Προφανώς κανένας προγραμματιστής ιστού δεν είναι ξένος στα REST APIs και τις προκλήσεις που δημιουργεί μια αποτελεσματική και αποδοτική λύση API .

Αυτές οι προκλήσεις περιλαμβάνουν:

  • Ταχύτητα (Χρόνοι απόκρισης API)
  • Τεκμηρίωση (Διαγραφή συνοπτικών εγγράφων, περιγραφή του API)
  • Αρχιτεκτονική και Αειφορία (Συντηρήσιμη και επεκτάσιμη βάση κώδικα)

Σε αυτό το σεμινάριο θα εξετάσουμε όλα τα παραπάνω χρησιμοποιώντας έναν συνδυασμό Node.js , MongoDB , Fastify και Swagger .

Ο πηγαίος κώδικας για το έργο είναι διαθέσιμος στο GitHub.

Πριν ξεκινήσουμε…

Θα πρέπει να έχετε κάποια γνώση για αρχάριους / ενδιάμεσους JavaScript , να έχετε ακούσει για το Node.js και το MongoDB και να γνωρίζετε τι είναι τα REST APIs .

Ακολουθούν ορισμένοι σύνδεσμοι για να σας ενημερώσουμε:

  • JavaScript
  • Node.js
  • MongoDB
  • API REST

Η τεχνολογία που θα χρησιμοποιούμε:

  • Στερεώστε
  • Μαγκούστα
  • Κομπάζω

Είναι καλή ιδέα να ανοίξετε τις παραπάνω σελίδες σε νέες καρτέλες, για εύκολη αναφορά.

Θα πρέπει να έχετε τα ακόλουθα εγκατεστημένα:

  • 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αντικαταστήσετε τη λέξη nodeστη γραμμή εντολών κατά την εκτέλεση του σεναρίου σας.

Για να ρυθμίσουμε το nodemon , πρέπει να προσθέσουμε την ακόλουθη γραμμή κώδικα στο package.jsonαρχείο μας , στο αντικείμενο scripts:

“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 παρέχει μια απλή λύση βάσει σχήματος για τη μοντελοποίηση των δεδομένων της εφαρμογής σας. Περιλαμβάνει ενσωματωμένο casting τύπου, επικύρωση, δημιουργία ερωτημάτων, άγκιστρα επιχειρηματικής λογικής και πολλά άλλα, εκτός κουτιού.

στερεώστε

Το Fastify είναι ένα πλαίσιο ιστού που εστιάζει στην παροχή της βέλτιστης εμπειρίας προγραμματιστή με την ελάχιστη επιβάρυνση και μια ισχυρή αρχιτεκτονική προσθηκών. Είναι εμπνευσμένο από το Hapi και το Express και, από όσο γνωρίζουμε, είναι ένα από τα ταχύτερα πλαίσια ιστού στην πόλη.

στερεώστε-διογκωτής

Δημιουργία τεκμηρίωσης Swagger για Fastify. Χρησιμοποιεί τα σχήματα που δηλώνετε στις διαδρομές σας για να δημιουργήσετε ένα έγγραφο που συμμορφώνεται με swagger.

κεραία

Το 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.

Στο επόμενο σεμινάριο, θα ενσωματώσουμε το GraphQL και τελικά θα ενσωματώσουμε το frontend με το Vue.js !