Express yourself with Node.js
Remembering the days when I was a student, I tried not to overload the lecture with unnecessary and complicated information that will not be useful to you in the next couple of months. On the other hand, it is encouraged to understand the details, therefore, I will attach links to additional materials and articles. The lecture is divided into blocks that are as independent of each other as possible, so if, after reading a title, you understand you're already familiar with the content, feel free to move on to the next block.
Good luck!
The student realized that a simple page begins to grow into a serious application with business logic and user management and at the same time they're not able to store large amounts of data on the client. So the student decides that it's time to develop the server!
And since the student writes in JS, the choice for the technology becomes obvious - Node.js. There is only one little thing left ― to learn this technology.
Working with Node.js
Theory
Difficulty: Like YouTube ads, you can skip it. Objectives: To understand what a server is and its role in the web.- Client ― can be a browser or any other software that makes requests through the web.
- Server ― a program or a part of it that accepts client requests via HTTP and responds with the content of requested resource or an error message.When a web developer says "server", they mean "web server". The same applies to "client", "application", etc.
- HTTP ― a set of rules for data communication on the Internet.
- HTTP-method ― indicates the desired action to be performed on the identified resource. The main ones include GET, POST, PUT, DELETE ― there are some more, but this is the basis that will be enough for a while
- HTTP-status ― issued by a server in response to a client's request and tells you the status of the requested operation. The most commonly used: 200, 301, 401, 404, 500.
Web theory
Let's take a look at an example of how the Web works:- user opened browser / launched client
- user opened the link / sent a GET request to the server via HTTP
- server processed the request / identified that a client requested a file and then found the file
If you write JS code and have already implemented your first HTML-page - it means that you know the client well enough. Here we will concentrate on the server-side(actually, we deal with the server because it's the topic of the lecture, but it does not matter).Server ― "what?" and "why?"
Simply put, when a user types a request in the address bar, the browser requests data from a remote computer (server) via HTTP. In response, it receives a file or data to display in a browser.To save and process all resources used on the web. It doesn't matter if it's one static page or a whole Facebook, everything is located on remote servers or web hosting services, and is displayed to the user only on request.The modern web server performs many actions for processing and storing information, works with static files and sensitive data that no one should have access to, performs complicated calculations that are too heavy for the client side, and much more. Therefore, depending on the complexity of the project and the tasks to be solved, for convenient work and efficiency, the server would be built according to a certain architecture: monolithic or microservice.
The history of Node.js
It was 2009 when the first version of Node.js was published. After that there have been some issues with rights, but we're less interested in politics than in the fact that Node.js executes JavaScript code outside a web browser:
JavaScript — client-side language#1
As a result - it has a large community that wants to write code on a server using a familiar language.Event Loop#2
JavaScript is a single-threaded language, which means that all operations, both synchronous and asynchronous, occur in a single thread that is never blocked. Non-blocking I/O model makes it lightweight and efficient.V8#3
An engine developed by the Google Chrome team to improve the performance of JavaScript in their browser. It's extremely fast and powerful because it's written in C++. Node.js runs the V8 engine outside the browser and this allows Node to be very performant.And now, thanks to all these factors, we can create complete web applications using JavaScript on both the client and server.
Preparation
Difficulty: Like Windows installation, easy but something can go wrong. Objectives: Install Node.jsTo start the installation process, just go to the official website https://nodejs.org, and download the installation file for your operating system, then follow the instructions.
Restart your computer after installation🖥️
Insufficient permissions⚠️
Invalid environment variables🚧
Run next commands:
# Print Node.js version
node -v
# Print npm version
npm -v
your versions may be newer
Did you notice npm? But did we install it? It's a package manager developed by the Node.js team and installed altogether with Node for easy management of modules. We'll talk about it in more detail in the next block, but now it's important to make sure it works.
Package managers
Difficulty: Like learning to play cards - you need to understand who is responsible for what. Objectives: Understand the definitions and start working with packages using npm- Modules — parts of a program / code, divided in separate blocks for further reuse.
- Packages — a collection of modules.
- Package managers ― a tool for installing, uninstalling, updating, versioning and validating packages and modules.
What are "packages" and why do they need "managers"?
1. In your project, you wrote a function to sort an array and reuse it in several places.
Congratulations, it's almost a package / module2. Later, you decided that the same function could help someone else, so you published it on StackOverflow as a reply.
Now your package / module is publicly available and other developers can simply copypaste it.3. And what if your solution was longer and took 400 lines of code? 500 lines? 1000?
It is not so convenient to copy such amounts of data manually.
4. Therefore, it is better to format your function correctly and publish it to a remote storage.
Now other programmers just need to know the name of your package / module and use npm to add it to their project, delete it if they don't like it, or upgrade to the next version if you implement it.
One more time - what is the difference between
packages
andmodules
? Believe me, even experienced developers are often confused, but the difference is simple -module
is an atom,package
is a molecule. In other words,package
consists of othermodules
, which are the smallest unit that solves one problem. Pretty simple, isn't it?Several years ago the block about package managers could have taken the whole lecture due to their variety, but today the situation has stabilized, and now to work with JS we have 2 leaders:
All npm packages are available in yarn, because they use the same data source. yarn was created only to improve the performance of npm. There is no actual difference between them, except for the subjective judgment of fans of a particular tool. In the future, you will have to decide on your own what is more convenient for you to work with, but today we will talk about npm, because of its relation with Node.js.
Working with npm
To find out the full list of commands available for npm, you can
and shouldread the documentation. In the lecture I'll provide most common ones:# project initialization npm init # package installation npm install package-name # package installation for development only npm install package-name --save-dev # package removal npm uninstall package-name # installation of a specific version of the package npm install package-name@version
- In conclusion, I would like to explain why people came up with "package managers", after all, we could add the code manually directly to the project without any problems:
Convenient#1
Instead of copypasting the code, there is a handy tool for this.Reduced project size#2
If we stored the code of all dependencies altogether with the project code, it would significantly increase its size, which would complicate the exchange of data within the team during developmentResolving conflicts#3
Due to the huge variety of packages, they can conflict with each other. Package managers deal with these problems.Optimization#4
Package Х can depend on package А and package Y can depend on package А. If we add packages X and Y to a project manually, we will have a duplicate of package А. Package managers solve duplicate issues and optimize the number of installed dependencies.Cross-platform#5
Different packages or their analogs may be required to work on different platforms, but managers can help and provide recommendations.
Introduction to Node.js
Difficulty: Like baseball rules - understood nothing, but very interesting. Objectives: Use Node.js packages to start simple HTTP server and deal with eventloop.Node.js - is a JavaScript runtime, where we can run scripts. Everything will work almost like in a browser, so the transition from frontend to backend should be as comfortable as possible.
To go to the environment you need to run the command node
in the terminal, then run any JS code. But obviously, this method is not the most convenient one and it is better to write code in files, that can then be run with the command node file_name.js
.
Experiment time! We already know how to use all of JS 's power with Node and that's awesome, but now we want to start implementing servers. Let's open an official documentation, to take a look at the list of built-in Node.js packages, and find out how to work with them. As we can see, there are too many of them, but we only need one to get started - http, that is responsible for working via HTTP. After reading the description we understand that it has many methods and it doesn't make sense for a beginner to consider all of them, instead let's write a practical example of starting an HTTP server:
// import of the http module
const http = require('http');
const port = 3000;
// callback on each HTTP request
const requestHandler = (request, response) => {
console.log(request.url);
response.end("How you doin'?");
};
// create HTTP-server
const server = http.createServer(requestHandler);
// run the server
server.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
The browser always sends a GET request to the specified URL and in addition requests favicon.ico, because it perceives any address as a page.
Server logs that we get from the requestHandler()
function
Meet Postman ― tool for convenient sending of HTTP-requests to a server. Via Postman we can easily change the URL, methods, headers, and body of requests.
We created a working server! Hooray! It is very simple and does not understand the difference between GEt and POST requests, does not know how to work with request body and much more, but it is the first one and we ❤️ it just for that, right? Let's teach the server to save all logs to a file, and also try another package fs, designed to work with the file system:
// import of the http module
const http = require('http');
// import of fs http module
const fs = require('fs');
const port = 3000;
// create a stream for writing to a file
const logFile = fs.createWriteStream('log.txt', { flags: 'a' });
// callback on each HTTP request
const requestHandler = (request, response) => {
console.log(request.url);
logFile.write(`Request url: ${request.url}\n`);
response.end("How you doin'?");
};
// create HTTP-server
const server = http.createServer(requestHandler);
// run the server
server.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
And what will happen if the writing to the file is not finished yet, and the server receives another request? Looking ahead, I will say that file system operations are asynchronous streams, and considering the single-threaded JS, they must block the server until completion. But this is not the case, and we can see this by bombarding the server with requests, and it will not cancel or delay any of them.
The circle in the center is a JS thread and it always performs only one operation at a time. On the left we see a queue of requests waiting to be fulfilled. When a synchronous request is received, it starts to be processed and upon completion starts the next request. In cases where the request causes asynchronous operations, it is delegated to the library libuv, which processes it in a separate thread, and upon completion, puts the callback in the same queue.
I tried to explain as simply as possible, but I'm not sure that's enough, so here's a more detailed video
Let's return to our code. It looks too cumbersome for the functionality it performs because it works with too low-level operations. We will talk in the next block about how to move to a higher level of abstraction with Express.js.
Introduction to Express.js
Difficulty: Like SuperMario level 🍄 - you need to lose several times to pass. Objectives: Install Express.js and start the HTTP server with it.Express.js is a package that provides many features that simplify developing Node.js applications. It increases the level of abstraction and we do not need to configure the network connection, parse headers, methods and query body, and more. To get started, let's install it in a project using npm.
npm i express
Create a new file and write the code for a basic HTTP server:
/* express.js */
// import of express
const express = require('express')
// express-app initialization
const app = express()
// listener for GET-requests to "/" route
app.get('/', function (req, res) {
res.send('Hello World')
})
// listener for PUT-requests to "/test" route
app.put('/test', function (req, res) {
res.send('Hello World PUT')
})
// run the server
app.listen(3000)
node express.js
Everything works! If we try to send requests to routes that aren't described in the code (for example GET: / users), we will receive a 404 response. In order to write something like this on nodes, we would need about 200 lines of code. Impressive, right? Let's move on.
Express.js extends common Node objects request and response, adding a lot of useful features right out of the box. For example, it parses query-parameters into an object and can take parts of a URL as parameters. In response there are convenient methods to format response data:
app.get('/:param', function (req, res) {
console.log(req.query); // query-params object
console.log(req.params.value); // value of param, located in the URL at the specified position '/:param'
res.status(200).json({ message: 'Hello World' }); // method "status()", sets status code for response, and "json()" configures Content-Type header for JSON format
})
5 lines, and how many benefits! This is how Express.js conquered the world. Currently, there are a huge number of npm packages for express, designed to solve various cases. Let's consider one of these - parsing the body of the query. Earlier to do this, we would need to add a package (officially recognized Best Practice) body-parser. But since it was widely used, the methods from it has been re-added under the express methods to provide request body parsing support out-of-the-box.
/* express.js */
// import of express
const express = require('express')
// express-app initialization
const app = express()
// parse application/x-www-form-urlencoded
app.use(express.urlencoded({ extended: false }))
// parse application/json
app.use(express.json())
// listener for GET-requests to "/" route
app.get('/', function (req, res) {
console.log(req.query)
res.send('Hello World')
})
// listener for PUT-requests to "/test" route
app.put('/test', function (req, res) {
console.log(req.body)
res.send('Hello World PUT')
})
// run the server
app.listen(3000)
node express.js
Send PUT-request with body:
Take a look at logs:
We explored enough basic express functionality to start with, so now we can take a look at how all this code would be scaled in real life.
Project
Difficulty: Like watching a painting artist - cool to observe. Objectives: To understand the structure of a typical project on Node.js/Express.jsAnd the best part 🍎. Here we will talk about the structure of the project, what, where, and why. For a quick start of a project, we can use express-generator, which is created by the Express team and generates a default starter on Express.js. It should be installed globally to be accessible anywhere from any project. To do this, add the -g
flag to the installation command:
npm i express-generator -g
Version:
Help:
Pay attention to the scripts
field in package.json, which contains npm scripts for working with the application. In order to start the server, we will need to use the command npm start
.
Article "Task automation with npm run"
1) delete the folder /public with all the contents 2) make changes to /routes/index.js
/* /routes/index.js */
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.send('Express');
});
module.exports = router;
3) make changes to app.js
/* app.js */
var express = require('express');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var app = express();
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use('/', indexRouter);
app.use('/users', usersRouter);
module.exports = app;
Currently, our project implements only one layer - routes, which should be responsible ONLY for working with HTTP-requests.
Why so radical? ...should be responsible ONLY for working.... Proper layering of the application is a guarantee of code that will be easy to maintain due to the convenient development and clear structure. If the layer only works with HTTP requests, then there should be no business logic or data handling, and vice versa.
Let's make our server smart and give it logic, business logic. In the world of Express applications, the layer that is responsible for business logic is called services, and because it is not generated automatically, we will create it manually.
1) Create a folder /services
2) Create a file /services/user.service.js
3) Add a function to the file and export it:
/* services/user.service.js */
const getName = user => user ? user.name : null;
module.exports = { getName };
4) Import the function from the service to the router, use it to get a user's name.
/* routes/users.js */
var express = require('express');
const { getName } = require('../services/user.service');
var router = express.Router();
router.get('/', function(req, res, next) {
res.send('Welcome');
});
/* GET users listing. */
router.post('/', function(req, res, next) {
const result = getName(req.body);
if (result) {
res.send(`Your name is ${result}`);
} else {
res.status(400).send("User wasn't sent");
}
});
module.exports = router;
5) Run the server npm start
6) Send POST-request to the /users route and take a look at the response that contains the sent name
Now the server is able to do something, time to think about how we want to store data! There is a repositories layer for this, and we are going to implement it.
1) Create folder /repositories
2) Create file /repositories/user.repository.js
3) Add a function to the file and export it:
/* repositories/user.repository.js */
const saveData = data => {
if (data) {
// code for saving data to the DB
console.log(`${data} is saved`);
return true;
} else {
return false;
}
}
module.exports = { saveData };
We won't deal with a database in this lecture, so our code is just a simulation of working with it. In reality, this layer should contain all the work with data: their retrieval, saving, and updating.
4) Import the function from the repository into the service and add the function saveName ()
:
/* services/user.service.js */
const { saveData } = require("../repositories/user.repository");
const getName = user => user ? user.name : null;
const saveName = user => {
if (user) {
const isSaved = saveData(user);
return isSaved;
} else {
return null;
}
}
module.exports = { getName, saveName };
5) Import the function from the service into the router and use it to work with the request body:
/* routes/users.js */
var express = require('express');
const { saveName } = require('../services/user.service');
var router = express.Router();
router.get('/', function(req, res, next) {
res.send("Hello!");
})
router.post('/', function(req, res, next) {
const result = saveName(req.body);
if (result) {
res.send("Data is saved");
} else {
res.status(400).send("User wasn't saved");
}
});
module.exports = router;
6) Run the server npm start
7) Send POST-request to the /users route and take a look at the response "Data is saved"
And the last layer is middlewares, examples of which we have already seen in this project. Let's find out how it is configured and what it does.
1) Create folder /middlewares
2) Create file /middlewares/auth.middleware.js
3) Add a function to the file and export it:
/* middlewares/auth.middleware.js */
const isAuthorized = (req, res, next) => {
if (req.headers.authorization === 'admin') {
next();
} else {
res.status(401).send();
}
};
module.exports = { isAuthorized };
middlewares - perform some actions on data coming through them and then they either pass the data to the next function in a stack or they end the request-response cycle. The middleware function takes three arguments: (req, res, next). We are already familiar with the first two objects, and the third is the callback function, which informs that all current operations are complete and we can proceed to the next step.
4) Import the middleware to the router and use it to secure the GET route:
/* routes/users.js */
var express = require('express');
const { getName } = require('../services/user.service');
const { isAuthorized } = require('../middlewares/auth.middleware');
var router = express.Router();
router.get('/', isAuthorized, function(req, res, next) {
res.send('Welcome');
});
/* GET users listing. */
router.post('/', function(req, res, next) {
const result = getName(req.body);
if (result) {
res.send(`Your name is ${result}`);
} else {
res.status(400).send("User wasn't sent");
}
});
module.exports = router;
To initialize the middleware, you can use the app.use
syntax, or simply pass it as a second argument to router.
5) Run the server npm start
6) Send GET-request to the /users route with the header Authorization: user
and take a look at response - 401 status
7) Send GET-request to the /users route with the header Authorization: admin
and take a look at response - we passed the check
And that's it, we covered the basics of Node.js applications structure. Now you're a true full-stack 😎
Conclusion
Difficulty: Like cool water on a summer day - a long-awaited relief. Objectives: Read, relax, wait half an hour and start the lecture againThat's it, congratulations! The lecture is completely scrolled, you don't need to read anything else. Quickly go to the tabs you opened during the lecture and read more. Then give yourself a break. At least half an hour... Yes, deadlines and all that, but it will be a bit more effective if you give yourself a break to think everything through, and then go back and do your homework.
Bonus
Difficulty: As at the kitchen - everything works out if you follow the recipe. Objectives: Deploy the Node.js application on Heroku.I was told that you all know how to deploy client applications, so why not learn to deploy Node.js servers?