Создание веб-API с помощью Node.js и Express
Используйте Express для Node.js, чтобы разрабатывать интерфейсы RESTful API. Создайте и настройте промежуточное ПО для добавления таких функций, как ведение журнала, проверка подлинности и авторизация, а также других технологий веб-разработки. В этом модуле используется JavaScript (CommonJS) с серверной платформой Express.js в среде выполнения Node.js.
Цели обучения
К концу этого модуля вы сможете:
- описывать основные понятия веб-платформы Express;
- настраивать ПО промежуточного слоя для управления обработкой запроса;
- передавать в ответ на HTTP-запросы содержимое различных типов, таких как текст и JSON;
- отправлять данные в конечную точку Express.
Предварительные требования
- На компьютере должны быть установлены Git и Node.js.
- Знакомство с редактированием текста и файлов кода в любом текстовом редакторе.
- Общее знакомство с протоколом HTTP.
- Опыт работы с помощью командной строки, включая операции Git.
How to create a REST API with Node.js and Express
Node.js is an ideal choice for developers who are looking to create fast and efficient web applications with RESTful APIs. In fact, it is the most widely used web development tool in the United States. But what makes Node.js so powerful? Why should you learn to use Node.js for building REST APIs, and what is the best way to go about it?
In this post, we’ll explore the answers to these questions and more. By the end, you’ll be able to set up a server, define routes, handle requests and responses, and work with databases. You can also get some hands-on experience by coding along with us and building a user management API with endpoints for user registration, login, profile management, and more.
Use the links below to navigate to the section that interests you most:
- What is a REST API?
- Popular HTTP methods
- What is Node.js?
- Why use Node.js to build your REST API?
- Prerequisites
- How to set up a Node.js app?
- How to create a user management API with Node.js and Express?
Let’s get started.
What is a REST API?
Related: What is an API?
REST, which stands for REpresentational State Transfer, is a software development architecture that defines a set of rules for communication between a client and a server. Let’s break this down a little more:
- A REST client is a code or app used to communicate with REST servers.
- A server contains resources that the client wants to access or change.
- A resource is any information that the API can return.
A REST API, also known as a RESTful API, is an API that conforms to the REST architecture. These APIs use the HTTP protocol to access and manipulate data on the server.
The essential components of a REST API include the HTTP method, endpoint, headers, and body. Here’s an example of a REST API that gives you a list of astronauts who are currently in space.
HTTP methods
The HTTP method defines the action the client wants to make on the server, which includes creating, reading, updating, or deleting resources (CRUD). Here are four HTTP methods that are commonly used in REST APIs:
- GET: used to retrieve resources.
- POST: used to add resources.
- PUT: used to update resources.
- DELETE: used to delete resources.
Now that we’ve covered the fundamentals of RESTful APIs, let’s look at why and how you can build them with Node.js.
What is Node.js?
By definition, Node.js is an open source and cross-platform JavaScript runtime environment that runs based on Chrome’s V8 engine.
To break this down a little further, you can:
-
- Quickly find the source code for Node.js (open source).
- Run Node.js on Linux, Windows, or macOS (cross-platform).
- Execute your JavaScript program or application on the server instead of the browser using Node.js (runtime environment).
Why should you use Node.js and Express to build your REST API?
Here are four key advantages of Node.js and Express:
- The ability to use a single language (JavaScript) for both client-side and server-side development.
- Fast and powerful performance, owing to the ability to run multiple requests in parallel.
- Middleware and routing capabilities that are built into Express to make API development quick and easy.
- A large, active community of developers contributing to the ecosystem.
As you develop the user management API, you’ll start seeing these benefits quickly. Let’s get started.
Prerequisites
To follow along, you need to have the following:
- A GitHub account to clone the companion repository
- A Postman account to access the user management API template
If you don’t have a Postman account yet, you can sign up for free here.
Setting up a Node.js app
Step 1: Install Node.js and NPM
The first thing we’ll need to do is install Node.js on our machine. You can download the latest LTS version from the official Node.js website. Follow the prompts in the Node.js Installer and customize the defaults, if necessary. When you’re done, you should have installed Node.js, as well as NPM (Node Package Manager). You can verify the installation by running the following commands in your terminal:
node -v npm -v
If you see the versions of Node.js and NPM show up, your installation was successful.
Step 2: Create a new project folder
Next, we’ll create a new folder for the project by running the following command in your terminal (note that entering this command as-is will name your project “node rest api,” but you can change the name, if you’d like):
mkdir node-rest-api
To navigate to your project, enter this command:
cd node-rest-api
Step 3: Initialize a new Node.js application
To initialize your app, run the following command in your terminal:
npm init
You will be prompted to enter your project name, description, and GitHub repository. You can accept the defaults by pressing Enter/Return, or customize them.
Next, open this project in your editor, where you will see a new file called package.json . This file contains the data you added about your project in the terminal. It also describes how you’re going to run the project and lists its dependencies (frameworks and libraries).
Step 4: Install Express and other dependencies
From here on, you can run all your commands in your editor’s terminal.
Run the following command to install the Express framework:
npm install express
Step 5: Import necessary modules
We’ll start by creating a new file named app.js in the root of the project directory. We’ll use this file to set up the app. Then, we’ll load the dependencies so we can use them. In the app.js file, add the following code to import Express:
const express = require(‘express’);
Now, let’s set up Express to create an app and configure it to parse requests with JSON payloads. Here’s the code you can add to do that:
const app = express (); app.use(express.json());
Step 6: Define a route that listens to requests
Now we need to make this application a server by getting it to listen for connections. To do this, we’ll connect to a port to listen for incoming requests.
Create a new file called config.js . In this file, we’ll add the following code to set a default port:
const PORT = process.env.PORT || 3000;
With the process.env.PORT variable, we set up the port automatically by allowing the API to be deployed to a cloud platform like AWS or Azure. In case the process.env.PORT variable is not set, we’ll default to using port 3000.
Next, we’ll add the following code to the the app.js file in order to set up the server to listen on the specified port:
app.listen(PORT, () => < console.log("Server Listening on PORT:", port); >);
Step 7: Define an endpoint
Let’s start by defining a status endpoint to ensure the API is working.
Express lets you define routes using the app.METHOD() function. Here, METHOD refers to the different HTTP methods, like GET, POST, PUT, and DELETE. For a GET request, you’d define the route by adding an app.get() function. This function has two parameters. We’ll use the first parameter to define the path. In this case, it is the /status endpoint:
app.get(“/status”, ());
Next, we’ll add a callback function as the second parameter, which defines what we will do when the request is called. This function has two parameters: the request object (which contains details like the HTTP method, headers, and request body) and the response object (which defines the information that we want to send). The response (res) object contains different methods of sending a response to the client, such as res.send() , res.json() , and res.render() .
Here’s what it looks like now:
app.get(“/status”, (request, response));
With response.send() , we then define the response we want to return. But since we want to send back JSON, we’ll need to first define a JSON object. So, we define a status variable and create an object:
response.send(status) is now a function that takes the JSON object as the argument. app.get(“/status”, (request, response) => < const status = < “Status”: “Running” >; response.send(status); >);
Challenge for you: Go to your Postman account and test the /status endpoint with the GET request method. You should get a response that says “Running,” as shown below:
If you receive the expected response, congratulations! You’ve just created your first API.
Similarly, you can use app.post() , app.put() , and app.delete() to handle other request methods.
Creating a user management API with Node.js and Express
Now, we’ll create the following API endpoints for user management:
- POST /signup : Registering a new user
- POST /login : Logging in
- GET /user : Retrieving a user’s profile (restricted to the user themselves)
- PATCH /users/:userId : Updating a user’s profile (restricted to the user themselves)
- GET /users/all : Retrieving all users (available to all users)
- PATCH /user/change-role/:userId : Updating a user’s role (restricted to admins)
- DELETE /user/:userId : Deleting a user (restricted to admins)
We’ll also look at how you can use JSON web tokens (JWTs) to set up authentication for certain endpoints.
We recommend trying out the user management API to understand what responses you can expect.
To get started, clone the companion repository that contains the entire code for the app and use this tutorial to understand the basics of how we go about creating the user management app.
Once you’ve cloned the repository, navigate to the e-commerce-service project, where you’ll find the following folders:
- authorization: contains everything related to the /signup and /login endpoints.
- common: contains middlewares and models used for all endpoints.
- storage: stores all the data for the app locally.
- users: contains everything related to all of the user endpoints.
Run npm install to install the following libraries that we will be using:
- Express: A Node.js framework for API development.
- Sequelize: A Node.js ORM (object-relational mapper) tool that helps with connecting to a database.
- SQLite3: A library that helps us to create a database, define tables, and manage a SQLite database file.
- jsonwebtoken: An implementation of JSON Web Token (JWT), an open standard (RFC 7519), used to securely transmit information between parties as a JSON object.
- AJV (Another JSON Schema Validator): A library that helps us validate the payload received against a structure defined using a JSON Schema.
Defining the user module
In real-world apps, we use databases to store data more efficiently. Since this is only a simple project, we wanted to keep things easy. So, we will build the API with SQLite, define the structure of the data in common/models/User.js , and store data in storage/data.db .
We’ll start by creating a new file called User.js . Next, in the same file, we will define our schema, which holds all the properties of a user (such as email , username , and password ).
We’ll also specify the data type—and whether the data can be left void—for every user property. For the user ID, we’ll use auto-increment to automatically create a unique number when a new record is inserted into the table:
const < DataTypes >= require("sequelize"); const < roles >= require("../../config"); const UserModel = < id: < type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true, >, username: < type: DataTypes.STRING, allowNull: false, unique: true, >>;
Similarly, you can also define other properties, such as email , password , age , role , firstName , and lastName , in the schema.
In the same User.js file, we will define an initialize method to register our schema with the Sequelize library and return a model. The returned model can then be used to manipulate (i.e., create, read, update, and delete) the data stored in the database.
Here’s how you can define a function to create a user:
module.exports = < initialize: (sequelize) =>< this.model = sequelize.define("user", UserModel); >, createUser: (user) => < return this.model.create(user); >>;
You can also define wrapper functions that consume the Sequelize library to get and update entries in the database, just like we did for createUser .
Now, we’re all set to define operations with the user module. Let’s look at how to define the /user endpoint.
We’ll start by creating a file named UserController.js , which will hold all the controller functions for the different routes related to the user module, like getAllUsers , updateUserDetails , etc.
Refer to the example below to understand how to set up the getAllUsers controller function. This function uses the findAllUsers function created in the User.js file above to fetch all the users from the table and return them in the response:
getAllUsers: (req, res) => < UserModel.findAllUsers(<>) .then((users) => < return res.status(200).json(< status: true, data: users, >); >) .catch((err) => < return res.status(500).json(< status: false, error: err, >); >); >,
We will import the UserController in the users/route.js file and define the route as shown below:
router.get("/", UserController.getAllUser);
Next, we need to register the users/routes.js file in our index.js file so that any calls made to the /user endpoints are routed to the correct file. To do that, we add the following code:
const UserRoutes = require("./users/routes"); app.use("/user", UserRoutes);
Defining the auth module
In this tutorial, we’ll implement authentication and authorization using JSON Web Tokens (JWT). JWT generates a valid token for the user and ensures that only authenticated users can access specific API endpoints.
When the user provides the necessary payload while registering or logging in, we will provide a token in return. The token typically expires after a period of time. To keep things simple, we will only focus on creating a single token.
Now we can define the /signup endpoint. The payload needs to contain firstName , lastName , email , username , password , role , and age . Here’s an example:
We will set up a controller to create a new user in the User table using helper functions defined in User.js —and also generate a JWT that will be returned as the response. Additionally, before storing the user in the table, we will hash the password using the SHA246 algorithm for better security:
module.exports = < register: (req, res) => < const payload = req.body; let encryptedPassword = encryptPassword(payload.password); let role = payload.role; if (!role) < role = roles.USER; >UserModel.createUser( Object.assign(payload, < password: encryptedPassword, role >) ) .then((user) => < // Generating an AccessToken for the user, which will be // required in every subsequent request. const accessToken = generateAccessToken(payload.username, user.id); return res.status(200).json(< status: true, result: < user: user.toJSON(), token: accessToken, >, >); >) .catch((err) => < return res.status(500).json(< status: false, error: err, >); >); >,
The generateAccessToken function used in the code above uses the jsonwebtoken library to generate a JWT that can be returned back to the user.
Now, we need to create the route and invoke the controller function that we just created. We can do that by adding the following code in authorization/routes.js :
router.post( "/signup", AuthorizationController.register);
Likewise, you can also define the /login endpoint.
We also need to register the authorization/routes.js file in our index.js file so that any calls made to the /login or /signup endpoints are routed to the correct file. To do that, we add the following code:
const AuthorizationRoutes = require("./authorization/routes"); const UserRoutes = require("./users/routes"); app.use("/", AuthorizationRoutes); app.use("/user", UserRoutes);
Defining permissions and validating data
We need to make sure that only authenticated users can securely access data in the Users resource.
Let us define a middleware that checks if the user is using a valid JWT. To do this, we’ll add the following code in common/middlewares/IsAuthenticatedMiddleware.js :
module.exports = < check: (req, res, next) => < const authHeader = req.headers['authorization']; // IF no auth headers are provided // THEN return 401 Unauthorized error if (!authHeader) < return res.status(401).json(< status: false, error: < message: 'Auth headers not provided in the request.' >>); >
Here, we’re checking for the presence of auth headers. If no auth headers are provided, we return a 401 unauthorized error. Likewise, we also add checks and return the same error when a bearer auth header is not provided, or when a bearer auth header is provided, but a token is not provided.
In common/middlewares/CheckPermissionMiddleware.js , we’ll check for permissions and validate if the user:
- exists in our database (to access the /user endpoint).
- has the required Admin role (to access the /users/all and /user/change-role/:userId endpoints).
Here, we’ll check if a user is in our database, and return a 403 forbidden error if not:
module.exports = < has: (role) => < return (req, res, next) =>< const < user: < userId >, > = req; UserModel.findUser(< id: userId >).then((user) => < // IF user does not exist in our database, means something is fishy // THEN we will return forbidden error and ask user to login again if (!user) < return res.status(403).json(< status: false, error: "Invalid access token provided, please login again.", >); >
Now, we’ll add the authentication middleware to the user’s module routes in /users/routes.js :
We can update the getAllUsers endpoint created to the code given below:
router.get("/",[IsAuthenticatedMiddleware.check, CheckPermissionMiddleware.has(roles.ADMIN)] , UserController.getAllUser);
This way, the /user/all endpoint can only be accessed by a logged in user who has the Admin role.
If you’ve tried this out on your own, congratulations on creating your first app using Node.js! You now have a solid foundation for building powerful and scalable APIs for your web applications.
You can continue building this app by adding or updating user properties, filtering the users by a particular property (with a query parameter), or connecting to another database like MongoDB etc or MySQL. You can also check out the product directory app added in the Products folder.
If you have any questions, suggestions, or feedback for us, feel free to drop them in the comments below. We’d love to hear from you!
Frequently asked questions about building APIs with Node.js
What database should I use with Node.js?
In this tutorial, we used SQLite to store the data in a file. SQLite is a great library that allows you to create a database, define tables, and manage a SQLite database file. It also helps you connect with a range of databases— including MongoDB, MySQL, and PostgreSQL—and run queries to manage data in the database. However, you can use any other database based on your requirements by installing the appropriate Node.js driver and modifying the controller functions to interact with the database.
What is the best framework for building REST APIs in Node.js?
While Feathers, Nest, LoopBack, and Moleculer are great frameworks for building REST APIs, here are a few reasons why Express is the best option:
- It offers features like Model-View-Controller (MVC), robust routing capabilities, and built-in middleware to enable you to create dynamic, scalable applications.
- It has a large Express community that can help you be faster and more efficient.
- It is easy to set up, learn, and use, making it an ideal choice for beginners.
This blog was co-authored by Arvind Kalra, who also worked on the companion code repository.
Лучшие практики Node.js — советы по структуре проектов
Привет, Хабр! Представляю вашему вниманию адаптированный перевод первой главы «Node.js Best Practices» автора Yoni Goldberg. Подборка рекомендаций по Node.js размещена на github, имеет почти 30 т. звезд, но до сих пор никак не упоминалась на Хабре. Предполагаю, что эта информация будет полезна, как минимум, для новичков.
1. Советы по структуре проектов
1.1 Структурируйте ваш проект по компонентам
Худшая ошибка больших приложений — это архитектура монолита в виде огромной кодовой базы с большим количеством зависимостей (спагетти-кодом), такая структура сильно замедляет разработку особенно внедрение новых функций. Совет — разделяйте ваш код на отдельные компоненты, для каждого компонента выделяйте собственную папку для модулей компонента. Важно чтобы каждый модуль остался маленьким и простым. В разделе «Подробнее» можно посмотреть примеры правильной структуры проектов.
В противном случае: разработчикам будет сложно развивать продукт — добавление нового функционала и внесение изменений в код будут производиться медленно и иметь высокий шанс поломки других зависимые компонент. Считается, что если бизнес-юниты не разделены, то могут возникнуть проблемы с масштабирование приложения.
Подробная информация
Объяснение одним абзацем
Для приложений среднего размера и выше монолиты действительно плохи — одна большая программа с множеством зависимостей просто сложна для понимания, да еще и часто приводит к спагетти-коду. Даже опытные программисты, которые умеют правильно «готовить модули», тратят много усилий на проектирование архитектуры и стараются тщательно оценить последствия каждого изменения в связях между объектами. Наилучшим вариантом является архитектура, базирующаяся на наборе небольших программ-компонент: разделите программу на отдельные компоненты, которые не делятся ни с кем своими файлами, каждая компонента должна состоять из небольшого количества модулей (например, модулей: API, сервиса, доступа к БД, тестирования и т.п.), так чтобы структура и состав компоненты были очевидны. Некоторые могут назвать эту архитектуру «микросервисной», но, важно понимать, что микросервисы — это не спецификация, которой вы должны следовать, а скорее набор некоторых принципов. По вашему желанию, вы можете принять на вооружение как отдельные из этих принципов, так и все принципы архитектуры микросервисов. Оба способа хороши если вы сохраняете сложность кода на низком уровне.
Самое меньшее, что вы должны сделать, это определить границы между компонентами: назначить папку в корне вашего проекта для каждого из них и сделать их автономным. Доступ к функционалу компоненты должен быть реализован только через публичный интерфейс или API. Это основа для того, чтобы сохранить простоту ваших компонентов, избежать «ада зависимостей» и дать вашему приложению дорасти до к полноценных микросервисов.
Цитата блога: «Масштабирование требует масштабирования всего приложения»
Из блога MartinFowler.comМонолитные приложения могут быть успешными, но люди все чаще испытывают разочарование в связи с ними, особенно когда задумываются о развертывании в облаке. Любые, даже небольшие, изменения в приложении требуют сборки и перевыкладки всего монолита. Часто трудно постоянно сохранять хорошую модульную структуру, при которой изменения в одном модуле не затрагивают другие. Масштабирование требует масштабирования всего приложения, а не только отдельных его частей, конечно, для такого подхода требуется больше усилий.
Цитата блога: «О чем говорит архитектура вашего приложения?»
Из блога uncle-bob… если вы бывали в библиотеке, то вы представляете ее архитектуру: парадный вход, стойки регистрации, читальные залы, конференц-залы и множество залов с книжными полками. Сама архитектура будет говорить: это здание — библиотека.
Так о чем же говорит архитектура вашего приложения? Когда вы смотрите на структуру каталогов верхнего уровня и файлы-модули в них они говорят: я — интернет-магазин, я — бухгалтерия, я — система управления производством? Или они кричат: я — Rails, я — Spring/Hibernate, я — ASP?
(Примечание переводчика, Rails, Spring/Hibernate, ASP — это фреймворки и веб-технологии).Правильная структура проекта с автономными компонентами
Неправильная структура проекта с группировкой файлов по их назначению
1.2 Разделяйте слои ваших компонентов и не смешивайте их со структурой данных Express
Каждый ваш компонент должен иметь «слои», к примеру, для работы с вебом, бизнес-логикой, доступом к БД, эти слои должны иметь свой собственный формат данных не смешанный с форматом данных сторонних библиотек. Это не только четко разделяет проблемы, но и значительно облегчает проверку и тестирование системы. Часто разработчики API смешивают слои, передавая объекты веб-слоя Express (к примеру, req, res) в бизнес-логику и в слой данных — это делает ваше приложение зависимым и сильно связанным с Express.
В противном случае: для приложения, в котором объекты слоев перемешаны, сложнее обеспечить тестирования кода, организацию CRON-тасков и других «неExpress» вызовов.
Подробная информация
Разделите код компонента на слои: веб, сервисы и DAL
Обратная сторона смешение слоев в одной gif-анимации
1.3 Оберните ваши базовые утилиты в пакеты npm
В большом приложении, состоящем из различных сервисов со своими репозиториями, такие универсальные утилиты, как логгер, шифрование и т.п., должны быть обернуты вашим собственным кодом и представлены как приватные npm-пакеты. Это позволяет делиться ими между несколькими кодовыми базами и проектами.
В противном случае: вам придется изобрести собственный велосипед для расшаривания этого кода между отдельными кодовыми базами.
Подробная информация
Объяснение одним абзацем
Как только проект начнет расти и у вас на разных серверах будут разные компоненты, использующие одни и те же утилиты, вы должны начать управлять зависимостями. Как можно без дублирования кода вашей утилиты между репозиториями позволить нескольким компонентам использовать ее? Для этого есть специальный инструмент, и называется он — npm…. Начните с обертывания сторонних пакетов утилит вашим собственным кодом, чтобы в будущем его можно было легко заменить, и опубликуйте этот код как частный пакет npm. Теперь вся ваша кодовая база может импортировать код утилит и использовать все возможности управления зависимостями npm. Помните, что есть следующие способы публиковать пакеты npm для личного использования, не открывая их для публичного доступа: частные модули, частный реестр или локальные пакеты npm.
Совместное использование собственных общих утилит в разном окружении
1.4 Разделяйте Express на «приложение» и «сервер»
Избегайте неприятной привычки определять все приложение Express в одном огромном файле, разделите ваш ‘Express’-код по крайней мере на два файла: объявление API (app.js) и код www-сервера. Для еще лучшей структуры размещайте объявление API в модулях компонент.
В противном случае: ваш API будет доступен для тестирования только через HTTP-вызовы (что медленнее и намного сложнее для создания отчетов о покрытии). Еще, предполагаю, не слишком большое удовольствие работать с сотнями строк кода в одном файле.
Подробная информация
Объяснение одним абзацем
Рекомендуем использовать генератор приложений Express и его подход к формированию базы приложения: объявление API отделено от конфигурации сервера (данных о порте, протоколе и т.п.). Это позволяет тестировать API без выполнения сетевых вызовов, что ускоряет выполнение тестирования и упрощает получение метрик покрытия кода. Это также позволяет гибко развертывать один и тот же API для разных сетевых настроек сервера. Бонусом вы так же получаете лучшее разделение ответственности и более чистый код.
Пример кода: объявление API, должно находиться в app.js
var app = express(); app.use(bodyParser.json()); app.use("/api/events", events.API); app.use("/api/forms", forms);
Пример кода: сетевых параметров сервера, должно находиться в /bin/www
var app = require('../app'); var http = require('http'); /** * Получаем порт из переменных окружения и используем его в Express. */ var port = normalizePort(process.env.PORT || '3000'); app.set('port', port); /** * Создать HTTP-сервер. */ var server = http.createServer(app);
Пример: протестируем свой API используя supertest (популярный пакет тестирования)
const app = express(); app.get('/user', function(req, res) < res.status(200).json(< name: 'tobi' >); >); request(app) .get('/user') .expect('Content-Type', /json/) .expect('Content-Length', '15') .expect(200) .end(function(err, res) < if (err) throw err; >);
1.5 Используйте безопасную иерархическую конфигурацию с учетом переменных окружения
Идеальная настройка конфигурации должна обеспечивать:
(1) считывание ключей как из конфигурационного файла, так и из переменных среды,
(2) хранение секретов вне кода репозитория,
(3) иерархическую (а не плоскую) структуру данных конфигурационного файла для облегчения работы с настройками.Есть несколько пакетов, которые могут помочь в реализации этих пунктов, такие как: rc, nconf и config.
В противном случае: несоблюдение этих требований к конфигурации приведет к срыву работы как отдельного разработчика, так и всей команды.
Подробная информация
Объяснение одним абзацем
Когда вы имеете дело с настройками конфигурации многие вещи могут раздражать и тормозить работу:
1. Задание всех параметров с использованием переменных окружения становится очень утомительным если требуется ввести 100+ ключей (вместо того, чтобы просто зафиксировать их в файле конфигурации), однако если конфигурация будет задаваться только в файлах настроек, то это может быть неудобно для DevOps. Надежное конфигурационное решение должно объединять оба способа: и файлы конфигураций, и переопределения параметров из переменных окружения.
2. Если конфигурационный файл является «плоском» JSON (т.е. все ключи записаны в виде единого списка), то при увеличении количества настроек с ним будет сложно работать. Решить эту проблему можно с помощью формирования вложенных структур содержащих группы ключей по разделам настроек, т.е. организовать иерархического JSON-структуру данных (см. пример ниже). Есть библиотеки, которые позволяют хранить такую конфигурацию в нескольких файлах и объединять данные из них во время выполнения.
3. Не рекомендуется хранить в конфигурационных файлах конфиденциальную информацию (такую как пароль БД), но однозначного удобного решения где и как хранить такую информацию — нет. Некоторые библиотеки конфигураций позволяют шифровать конфигурационные файлы, другие шифруют эти записи во время git-коммитов, а можно вообще не сохранять секретные параметры в файлах и задавать их значения во время развертывания через переменные среды.
4. Некоторые расширенные сценарии конфигураций требуют ввода ключей через командную строку (vargs) или синхронизируют конфигурационные данные через централизованный кэш, такой как Redis, чтобы несколько серверов использовали одни и те же данные.
Есть npm-библиотеки, которые помогут вам с реализацией большинства этих рекомендаций, советуем взглянуть на следующие библиотеки: rc, nconf и config.
Пример кода: иерархическая структура помогает находить записи и работать с объемными файлами конфигураций
< // Customer module configs "Customer": < "dbConfig": < "host": "localhost", "port": 5984, "dbName": "customers" >, "credit": < "initialLimit": 100, // Set low for development "initialDays": 1 >> >
(Примечание переводчика, в классическом JSON-файле нельзя использовать комментарии. Вышеприведенный пример взят из документации библиотеки config, в которой добавлен функционал предварительной очистки JSON-файлов от комментариев. Поэтому пример вполне рабочий, однако линтеры, такие как ESLint, с настройками по умолчанию могут «ругаться» на подобный формат).
Послесловие от переводчика:
- В описании проекта написано, что перевод на русский язык уже запущен, но этого перевода я там не нашла, поэтому и взялась за статью.
- Если перевод кажется вам очень кратким, то попробуйте развернуть подробную информацию в каждом разделе.
- Простите, что иллюстрации оставлены без перевода.
- Node.js
- JavaScript
- разработка
- best practices
- архитектура приложений
- server-side javascript
Первое рабочее место или как начать разработку API на Node.js
В данной статье хотел бы поделиться своими эмоциями и приобретенными навыками в разработке первого REST API на Node.js с использованием TypeScript, как говорится, с нуля. История достаточно банальная: «Закончил университет, получил диплом. Куда же пойти работать?» Как можно было догадаться меня проблема не обошла стороной, пусть думать особо и не пришлось. Позвал к себе на стажировку разработчик (выпускник той же специальности). Полагаю, что это достаточно распространенная практика и существует множество подобных историй. Я, недолго думая, решил попробовать свои силы и пошел…
День первый. Знакомство с Node.js
Пришёл я на back-end разработку. В данной IT-компании используют платформу Node.js, с которой я абсолютно не был знаком. Я немного убежал вперед, забыв рассказать читателю, что никогда и ничего не разрабатывал на JavaScript (за исключением пары скриптов с копированным кодом). Алгоритм работы и архитектуру веб-приложений в целом я понимал, так как разрабатывал CRUD на Java, Python и Clojure, но этого было недостаточно. Поэтому первый день я полностью посвятил изучению Node.js, очень помог этот скринкаст.
Параллельно изучая веб-фреймворк Express, менеджер пакетов npm, а также такие файлы как package.json и tsconfig.json, голова просто шла кругом от количества информации. Очередной урок, что усвоение всего материала одновременно задача близкая к невозможной. К концу дня я все же справился с настройкой окружения и смог запустить express веб-сервер! Но радоваться было рано, потому что уходил домой с полным ощущением непонимания. Чувство, что я утопал в огромном мире JS не покидало меня ни на минуту, поэтому необходима была перезагрузка.
День второй. Знакомство с TypeScript
Та самая перезагрузка последовала именно в этот день. К этому моменту я полностью узнал свою задачу, к ней мы перейдем чуть ниже. Зная, что предстоит писать не на чистом JavaScipt, обучение от Node.js плавно перетекло к языку TypeScript, а именно, его особенностям и синтаксису. Здесь я увидел долгожданные типы, без которых не представлял программирование буквально 2 дня тому назад не в функциональных языках программирования. Это и было моим самым большим заблуждением, которое мешало мне понять и усвоить код, написанный на JavaScript в первый день.
Ранее писал по большей части на объектно-ориентированных языках программирования, таких как Java, C++, C#. Осознав возможности TypeScript, я почувствовал себя в своей тарелке. Этот язык программирования буквально вдохнул в меня жизнь этой сложной среды, как мне казалось на тот период. Под занавес дня я полностью настроил окружение, запустил сервер (уже на TypeScript), подключил необходимые библиотеки, о которых ниже расскажу. Итог: готов к разработке API. Переходим непосредственно к разработке…
Разработка API
Объяснение принципа работы и прочие разъяснение о том, что же такое REST API мы оставим, так как на форуме очень много статей об этом с примерами и разработкой на различных языках программирования.
Задача стояла следующая:
Сделать сервис с REST API. Авторизация по bearer токену (/info, /latency, /logout). Настроенный CORS для доступа с любого домена. DB — MongoDB. Токен создавать при каждом заходе.
Описание API:
- /signin [POST] — запрос bearer токена по id и паролю // данные принимает в json
- /signup [POST] — регистрация нового пользователя: // данные принимает в json
- /info [GET] — возвращает id пользователя и тип id, требует выданный bearer токен в аутентификации
- /latency [GET] — возвращает задержку (ping), требует выданный bearer токен в аутентификации
- /logout [GET] — с паметром all: true — удаляет все bearer токены пользователя или false — удаляет только текущий bearer токен
Средства реализации
Итак, я упоминал, что уже изучил во второй день несколько библиотек (фреймворков), с этого и начнем. Для роутинга я выбрал routing-controllers, руководствовался большим сходством с декораторами из Spring Framework (Java). В качестве ORM я выбрал typeorm, хоть и работа с MongoDB в экспериментальном режиме, но для такой задачи вполне достаточно. Для генерации токенов использовал uuid, переменные загружаются с помощью dotenv.
Начальный запуск веб-сервера
Обычно, используется express в чистом виде, но я упоминал выше фреймворк Routing Controllers, который позволяет нам создать express сервер следующим образом:
//Создаем приложение Express const app = createExpressServer( < //Префикс routePrefix: process.env.SERVER_PREFIX, //Инициализируем ошибки defaults: < nullResultCode: Number(process.env.ERROR_NULL_RESULT_CODE), undefinedResultCode: Number(process.env.ERROR_NULL_UNDEFINED_RESULT_CODE), paramOptions: < required: true >>, //Проверка авторизации пользователя authorizationChecker: authorizationChecker, //Контроллер controllers: [UserController] >); //Запуск приложения app.listen(process.env.SERVER_PORT, () => < console.log(process.env.SERVER_MASSAGE); >);
- routePrefix — это просто префикс в вашем url после адреса сервера, например: localhost:3000/prefix
- defaults — ничего интересного, просто инициализируем коды ошибок
- authorizationChecker — прекрасная возможность фреймворка проверять авторизацию пользователя, далее рассмотрим более подробно
- controllers — одно из основных полей, где мы указываем контроллеры, используемые в нашем приложении
Подключение к БД
Ранее, мы уже запустили веб-сервер, поэтому продолжим подключением к базе данных MongoDB, предварительно развернув на локальном сервере. Установка и настройка подробно описаны в официальной документации. Мы же непосредственно рассмотрим подключение с помощью typeorm:
//Подключение БД createConnection(< type: 'mongodb', host: process.env.DB_HOST, database: process.env.DB_NAME_DATABASE, entities: [ User ], synchronize: true, logging: false >).catch(error => console.log(error));
Все достаточно просто, необходимо указать несколько параметров:
- type — БД
- host — ip адрес, на котором вы развернули базу данных
- database — название непосредственно базы, которую предварительно создали в mongodb
- synchronize — автоматическая синхронизация с БД (Примечание: миграции на тот момент было тяжеловато освоить)
- entities — здесь мы указываем сущности, c помощью которых производится синхронизация
Теперь соединяем запуск сервера и подключение к БД. Отмечу, что импорт ресурсов отличается от классического, используемого в Node.js. В итоге получаем следующий запускаемый файл, в моем случае main.ts:
import 'reflect-metadata'; import * as dotenv from 'dotenv'; import < createExpressServer >from 'routing-controllers'; import < createConnection >from 'typeorm'; import < authorizationChecker >from './auth/authorizationChecker'; import < UserController >from './controllers/UserController'; import < User >from './models/User'; dotenv.config(); //Подключение БД createConnection(< type: 'mongodb', host: process.env.DB_HOST, database: process.env.DB_NAME_DATABASE, entities: [ User ], synchronize: true, logging: false >).catch(error => console.log(error)); //Создаем приложение Express const app = createExpressServer( < //Префикс routePrefix: process.env.SERVER_PREFIX, //Инициализируем ошибки defaults: < nullResultCode: Number(process.env.ERROR_NULL_RESULT_CODE), undefinedResultCode: Number(process.env.ERROR_NULL_UNDEFINED_RESULT_CODE), paramOptions: < required: true >>, //Проверка авторизации пользователя authorizationChecker: authorizationChecker, //Контроллер controllers: [UserController] >); //Запуск приложения app.listen(process.env.SERVER_PORT, () => < console.log(process.env.SERVER_MASSAGE); >);
Сущности
Напомню, что задача состоит в аутентификации и авторизации пользователей, соответственно нам необходима сущность: Пользователь (User). Но это еще не все, так как каждый пользователь имеет токен и не один! Поэтому необходимо создать сущность Токен (Token).
import < ObjectID >from 'bson'; import < IsEmail, MinLength >from 'class-validator'; import < Column, Entity, ObjectIdColumn >from 'typeorm'; import < Token >from './Token'; //Сущность пользователь @Entity() export class User < //Уникальный идентификатор @ObjectIdColumn() id: ObjectID; //Email для аутентификации пользователя @Column() @IsEmail() email: string; //Пароль пользователя @Column(< length: 100 >) @MinLength(2) password: string; //Токены пользователя @Column() token: Token; >
В таблице User мы создаем поле — массив тех самых токенов для пользователя. Также мы подключаем calss-validator, так как необходимо, чтобы пользователь осуществлял вход через email.
import < Column, Entity >from 'typeorm'; //Сущность для токенов @Entity() export class Token
База выглядит следующим образом:
Авторизация пользователя
Для авторизации мы используем authorizationChecker(один из параметров при создании сервера см. выше), для удобства вынесем в отдельный файл:
import < Action, UnauthorizedError >from 'routing-controllers'; import < getMongoRepository >from 'typeorm'; import < User >from '../models/User'; export async function authorizationChecker(action: Action): Promise < let token: string; if (action.request.headers.authorization) < //Получаем текущий токен token = action.request.headers.authorization.split(" ", 2); const repository = getMongoRepository(User); const allUsers = await repository.find(); for (let i = 0; i < allUsers.length; i++) < if (allUsers[i].token.accessToken.toString() === token[1]) < return true; >> > else < throw new UnauthorizedError('This user has not token.'); >return false; >
После аутентификации у каждого пользователя появляется свой токен, поэтому мы можем из заголовков (headers) ответа вытащить необходимый токен, выглядит он примерно так: Bearer 046a5f60-c55e-11e9-af71-c75526de439e. Теперь мы можем проверить, существует ли данный токен, после чего функция возвращает информацию об авторизации: true — пользователь авторизован, false — пользователь не авторизован. В приложении мы можем использовать очень удобный декоратор в контроллере: @Authorized(). В этот момент и будет вызвана функция authorizationChecker, которая вернет ответ.
Логика
Для начала я хотел бы описать бизнес логику, так как контроллер — это одна строчка вызова методов ниже представленного класса. Также, в контроллере мы будем принимать все данные, в нашем случае это будет JSON и Query. Рассмотрим по отдельным задачам методы, а в конце сформируем итоговый файл, который назван UserService.ts. Отмечу, что на тот момент знаний для устранения зависимостей попросту не хватало. Если вы не встречались с термином инъекция зависимостей, очень советую прочитать об этом. На данный момент пользуюсь DI-фреймворком, т. е. использую контейнеры, а именно инъекцию через конструкторы. Вот, я считаю, хорошая статья для ознакомления. Возвращаемся к задаче.
-
/signin [POST] — аутентификация зарегистрированного пользователя. Все очень просто и прозрачно. Нам всего лишь нужно найти данного пользователя в базе данных и выдать новый токен. Для чтения и записи используется MongoRepository.
async userSignin(user: User): Promise < //Создаем Mongo repository const repo = getMongoRepository(User); //Ищем введенный логин и пароль в БД let userEmail = await repo.findOne(< email: user.email, password: user.password >); if (userEmail) < //Создаем токен userEmail = await this.setToken(userEmail); //Обновляем токены в базе repo.save(userEmail); return userEmail.token.accessToken; >return process.env.USER_SERVICE_RESPONSE; >
async userSignup(newUser: User): Promise < //Создаем Mongo repository const repo = getMongoRepository(User); //Проверяем на совпадение email (Чтобы не было 2 пользователя с одним email) const userRepeat = await repo.findOne(< email: newUser.email >); if (!userRepeat) < //Создаем токен newUser = await this.setToken(newUser); //Добавляем в базу const addUser = getMongoManager(); await addUser.save(newUser); return newUser.token.accessToken; >else < return process.env.USER_SERVICE_RESPONSE; >>
async getUserInfo(req: express.Request): Promise < //Создаем Mongo repository const repository = getMongoRepository(User); //Поиск по текущему токену const user = await this.findUser(req, repository); return user; >private async findUser(req: express.Request, repository: MongoRepository): Promise < if (req.get(process.env.HEADER_AUTH)) < //Получаем токен const token = req.get(process.env.HEADER_AUTH).split(' ', 2); //Получаем пользователей из базы const usersAll = await repository.find(); //Ищем пользователя for (let i = 0; i < usersAll.length; i++) < if (usersAll[i].token.accessToken.toString() === token[1]) < return usersAll[i]; >> > >
getLatency(): Promise < function update(progress: number, total: number): void < console.log(progress, '/', total); >const latency = ping(< address: process.env.PING_ADRESS, attempts: Number(process.env.PING_ATTEMPTS), port: Number(process.env.PING_PORT), timeout: Number(process.env.PING_TIMEOUT) >, update).then(result => < console.log('ping result:', result); return result; >); return latency; >
async userLogout(all: boolean, req: express.Request): Promise < //Создаем Mongo repository const repository = getMongoRepository(User); //Поиск по текущему токену const user = await this.findUser(req, repository); if (all) < //Если true удаляем все токены user.token.accessToken = process.env.GET_LOGOUT_TOKEN; user.token.refreshToken = process.env.GET_LOGOUT_TOKEN; //Сохраняем изменения repository.save(user); >else < //Если false удаляем только текущий user.token.accessToken = process.env.GET_LOGOUT_TOKEN; //Сохраняем изменения repository.save(user); >>
Контроллер
Многим не нужно объяснять для чего нужен и как используется контроллер в паттерне MVC, но два слова я все же скажу. В кратце, контроллер является связующим звеном между пользователем и приложением, который переправляет данные между ними. Выше полностью была описана логика, методы которой вызываются в соответствии с роутами, состоящий из URI и ip сервера (пример: localhost:3000/signin). О декораторах в контроллере я уже упоминал ранее: Get, POST, @Authorized и самый важный из них это @JsonController. Еще одна очень важная фишка данного фреймворка состоит в том, что если мы хотим отправлять и получать JSON, то используем именно данный декоратор вместо Controller.
import * as express from 'express'; import < Authorized, Body, Get, Header, JsonController, NotFoundError, Post, QueryParam, Req, UnauthorizedError >from 'routing-controllers'; import < IPingResult >from '@network-utils/tcp-ping'; import < User >from '../models/User'; import < UserService >from '../services/UserService'; //Декоратор для работы с JSON @JsonController() export class UserController < userService: UserService //Конструткор контроллера constructor() < this.userService = new UserService(); >//Вход пользователя @Post('/signin') async login(@Body() user: User): Promise < const responseSignin = await this.userService.userSignin(user); if (responseSignin !== process.env.USER_SERVICE_RESPONSE) < return responseSignin; >else < throw new NotFoundError(process.env.POST_SIGNIN_MASSAGE); >> //Регистрация пользователя @Post('/signup') async registrateUser(@Body() newUser: User): Promise < const responseSignup = await this.userService.userSignup(newUser); if (responseSignup !== process.env.USER_SERVICE_RESPONSE) < return responseSignup; >else < throw new UnauthorizedError(process.env.POST_SIGNUP_MASSAGE); >> //Возвращает авторизированного пользователя @Get('/info') @Authorized() async getId(@Req() req: express.Request): Promise < return this.userService.getUserInfo(req); >//Время задержки сервера @Authorized() @Get('/latency') getPing(): Promise < return this.userService.getLatency(); >@Get('/logout') async deleteToken(@QueryParam("all") all: boolean, @Req() req: express.Request): Promise < this.userService.userLogout(all, req); >>
Заключение
В данной статье я хотел отразить больше не техническую составляющую правильного кода или чего-то такого, а просто поделиться тем, что человек может с абсолютного нуля за пять дней собрать веб-приложение, использующее базу данных и содержащее хоть какую-то, но логику. Только вдумайтесь ни один инструмент не был знаком, вспомните себя или просто поставьте на мое место. Ни в коем случае это не случай, который говорит: «я самый лучший, вы так никогда не сможете». Наоборот, это крик души человека, который в данный момент находится в полном восторге от мира Node.js и делится с Вами этим. А также тем, что ничего нет невозможного, нужно просто брать и делать!
Конечно, нельзя отрицать, что автор ничего не знал и первый раз сел писать код. Нет, знания ООП, принципы работы REST API, ORM, база данных присутствовали в достаточном объеме. И это может говорить только о том, что средство достижения результата абсолютно не играет никакой роли и высказывания в стиле: «Не пойду на эту работу, там язык программирования, который я не учил», для меня теперь просто проявление человеком не слабости, а скорее защиты от незнакомой внешней среды. Да что там скрывать, страх присутствовал и у меня.
Подведем итоги. Хочу посоветовать студентам и людям, которые еще не начали свою карьеру в IT, не бояться средств разработки и неизвестных технологий. Вам обязательно помогут старшие товарищи (если повезет также как и мне), подробно разъяснят и ответят на вопросы, потому что каждый из них оказывался в таком положении. Но не забывайте, что Ваше желание — это самый важный аспект!