The code of this project has been open source. See:
Front-end engineering: vuE3 – TS -blog-frontend
Back-end engineering: Express-blog -backend
Database initialization script: pay attention to the public number front si Nan, reply keyword “blog database script”, can be obtained.
Express is a fast, open and minimalist Web development framework based on node. js platform. It has been updated to version 5.x.
The backend of my blog was actually developed quite early, and was almost fully functional by the end of nineteen nineteen, using Express 4.x.
When building back-end services with Express, the main concerns are:
- Routing middleware and controllers
- SQL processing
- The response returns a body data structure
- Error code
- Web security
- Environment variables/configuration
Routing and Controller
Routing is basically divided by module or function.
The first is to divide the level 1 route by module, each module’s sub-function is equivalent to using the level 2 route processing.
For a simple example, the /article route starts with the article module, and /article/add is used to add article functionality.
The controller concept is borrowed from other languages, Express doesn’t say exactly what a controller is, but it seems to me that the routing middleware’s processing modules/functions are the controller concept.
Here are some of the controllers used in this project.
const BaseController = require('.. /controllers/base');
const ValidatorController = require('.. /controllers/validator');
const UserController = require('.. /controllers/user');
const BannerController = require('.. /controllers/banner');
const ArticleController = require('.. /controllers/article');
const TagController = require('.. /controllers/tag');
const CategoryController = require('.. /controllers/category');
const CommentController = require('.. /controllers/comment');
const ReplyController = require('.. /controllers/reply');
module.exports = function(app) {
app.use(BaseController);
app.use('/validator', ValidatorController);
app.use('/user', UserController);
app.use('/banner', BannerController);
app.use('/article', ArticleController);
app.use('/tag', TagController);
app.use('/category', CategoryController);
app.use('/comment', CommentController);
app.use('/reply', ReplyController);
};
Copy the code
BaseController
The BaseController is used as the first level to do a basic checksum intercept on all requests.
In fact, it is mainly for some sensitive interfaces (such as background maintenance) to do a permission check.
Permission control, I design is relatively simple and rough, because I only reserve a user Tusi in the database table, the associated role is also the only use of admin. After all, while the ability to open user registration is not currently considered, having an administrative user is basically enough.
So my design is: as long as I login successfully within the validity period, I have the right to operate sensitive interface, otherwise I have no right to operate!
BaseController works as follows:
The body code structure of BaseController looks like this:
router.use(function(req, res, next) {
// authMap maintains a list of sensitive interfaces
const authority = authMap.get(req.path);
// First check whether the interface is sensitive
if (authority) {
// Interface that needs to verify identity
if (req.cookies.token) {
// Get the token for verification
dbUtils.getConnection(function (connection) {
req.connection = connection;
// We can check the identity of the database
connection.query(indexSQL.GetCurrentUser, [req.cookies.token], function (error, results, fileds) {
// An error code is returned if the authentication succeeds})})}else {
return res.send({
...errcode.AUTH.UNAUTHORIZED
});
}
} else {
// Is not a sensitive interface and does not verify identity
if (req.method == 'OPTIONS') {
// OPTIONS requests cannot connect to the database, otherwise the database connection will collapse
next();
} else {
// Get connection from mysql connection pool
dbUtils.getConnection(function (connection) {
req.connection = connection;
next();
}, function (err) {
returnres.send({ ... errcode.DB.CONNECT_EXCEPTION }); })}}}Copy the code
As noted, BaseController performs an identity check on sensitive interfaces to prevent system data from being hacked by malicious HTTP requests.
20220218 update
After the function is implemented according to the above logic and goes online, the service runs for a period of time (3 to 5 days), and the service request becomes unresponsive.
In fact, I can feel that the mysql connection pool is not properly released.
However, since MY initial approach was to mount the connection to REQ in BaseController and release the connection after the specific business controller executed the SQL query, this basic usage process is also described in the following section.
It would take a lot of code change to completely change this method, so I put it off. If I found a problem, I could restart the service by PM2. More recently, we have refactored everything, see Refactor: Refactoring SQL calls.
Business Controller
The front end will be divided into modules, and so will the back end. There are many business modules, such as articles, categories, tags, and so on. These can all be handled by different controllers.
The general structure of a service Controller is as follows. Each subroute corresponds to one function:
/ * * *@param {Number} Count Number of queries *@description Get the top N read articles */ based on the passed count
router.get('/top_read'.function (req, res, next) {
// Business code
}
/ * * *@param {Number} PageNo page number *@param {Number} PageSize Number of pages *@description Paging query articles */
router.get('/page'.function (req, res, next) {
// Business code
}
/ * * *@param {Number} Id Indicates the ID * of the current article@description Query the id of the previous and next articles */
router.get('/neighbors'.function (req, res, next) {
// Business code
}
Copy the code
SQL processing
For SQL, I didn’t use ORM directly. Because I think mysql foundation is not very good, but also need to write more SQL statements to practice, so I only use a mysql library.
Install mysql dependency:
npm install --save mysql
Copy the code
In simple use, you can create the connection directly and then execute the SQL statement:
var mysql = require('mysql');
var connection = mysql.createConnection({
host : 'localhost'.user : 'me'.password : 'secret'.database : 'my_db'
});
connection.connect();
connection.query('SELECT 1 + 1 AS solution'.function (error, results, fields) {
if (error) throw error;
console.log('The solution is: ', results[0].solution);
});
connection.end();
Copy the code
In fact, it is recommended to use connection pool, which can avoid the repeated application to MySQL for connection, realize the reuse of connection, and the response speed is also faster!
var mysql = require('mysql');
varpool = mysql.createPool(...) ; pool.getConnection(function(err, connection) {
if (err) throw err; // not connected!
// Use the connection
connection.query('SELECT something FROM sometable'.function (error, results, fields) {
// When done with the connection, release it.
connection.release();
// Handle error after the release.
if (error) throw error;
// Don't use the connection here, it has been returned to the pool.
});
});
Copy the code
In practice, I execute pool.getConnection in BaseController, and then mount the Connection object to the REQ object. The subsequent routing middleware can directly obtain the Connection from the REQ object, which can save one layer of nested callback. It also avoids writing this section of duplicate getConnection code for every business code.
BaseController key code:
// Get connection from mysql connection pool
dbService.getConnection(function (connection) {
req.connection = connection;
next();
}, function (err) {
returnres.send({ ... errcode.DB.CONNECT_EXCEPTION }); })Copy the code
The business gets the Connection object directly from req:
router.get('/page'.function (req, res, next) {
const connection = req.connection;
const pageNo = Number(req.query.pageNo || 1);
const pageSize = Number(req.query.pageSize || 10);
connection.query(indexSQL.GetPagedArticle, [(pageNo - 1) * pageSize, pageSize], function (error, results, fileds) {
connection.release();
// Other business codes
})
Copy the code
SQL statements are written mainly in the form of strings and pass? As a parameter slot, it receives dynamic values.
For example, for a logical delete statement, we would write:
// Logical delete/restore
UpdateArticleDeleted: 'UPDATE article SET deleted = ? WHERE id = ? '.Copy the code
The first one? Is the value reserved for field deleted, second? Pass the specific ID value.
The parameter pass is carried by the second parameter of connection.query.
Note that this parameter is an array in which the values in the SQL string are replaced in left-to-right order by? , into a real executable SQL statement.
connection.query(indexSQL.UpdateArticleDeleted, [params.deleted, params.id], function (error, results, fileds) {})
Copy the code
Connection.query Remember to call Connection.release to release the connection after the callback.
Another thing to watch out for is MySQL transaction handling. For transactions, the initial focus is on these three apis! I will refer to the specific application scenarios in the future, but I will not expand here!
/ / transaction, corresponding to the MySQL connection. The begin statements beginTransaction (); Connection.mit (); Connection.rollback ();Copy the code
20220218 update
In order to preserve a shift in the way I use mysql in this project, THE previous mysql call process is described as the original idea, and these are the key points.
- BaseController obtains the connection object of the mysql pool and mounts it to the REQ object for future services.
- When the service Controller interacts with mysql, it only needs to obtain the Connection from the REQ object and execute the SQL statement through connection.query.
- After executing the SQL statement, the service Controller releases the connection.
- In a transaction scenario, a unified release releases the Connection after the transaction, rather than each Query releasing the connection itself.
Although such a design eliminates the need to execute getConnection (one less callback) in the specific business Controller, there are still loopholes in the control of Connection.release (). Once the business caller forgets to call release(), the service may be unavailable. And some services do not need to interact with mysql, you must remember release(), although you can use some configuration fields to avoid, but also can not fundamentally solve the problem!
Therefore, my modification plan is as follows:
- The general principle is high cohesion, low coupling.
- Encapsulate the mysql query process, including getConnection, Query, release and other key actions in the encapsulated code control, external only some encapsulated methods, so that the caller does not have to worry about forgetting some key operations (such as
release()
). - Key apis Promise, so that in some complex asynchronous processes can achieve more with less, especially when it comes to transaction processing!
See db.js for the core code
Response return body
The data structure of the response return body needs to be agreed on by the front and back ends. Only when the norms are agreed, can the two sides cooperate closely and orderly. Typically, error codes, information, data fields are involved.
The error code and message fields should be common. The data part of data, depending on business needs, may have a variety of cases, such as array structure, object structure, or common data type.
{
code: "0".message: "Query successful".data: {
id: 1.name: 'xxx'}}Copy the code
Error code
Error codes are an essential part of the backend specification. Error codes are designed to quickly locate problems and provide analysis and statistics for some service monitoring systems.
Every programmer has his or her own coding style. In the case of error codes, I used semantic attribute names to locate error codes. Typically, an error code is paired with an error message, the MSG field below.
module.exports = {
DB: {
CONNECT_EXCEPTION: {
code: "1".msg: "Database connection exception"}},AUTH: {
UNAUTHORIZED: {
code: "000001".msg: "Sorry, you are not authorized yet."
},
AUTHORIZE_EXPIRED: {
code: "000002".msg: "Authorization has expired."
},
FORBIDDEN: {
code: "000003".msg: "Sorry, you don't have permission to access this content."}}},Copy the code
Error code design also has the advantage of facilitating mapping.
What does that mean? The back end returns error code -1 and tells the front end that the error message is a database connection exception through the MSG field. But does the front end want to give users such direct and rude feedback? Sometimes, I think, it’s not necessary, but a gentle reminder to calm the user down.
For instance,
So, with error codes, the front end can be more flexible and play with error messages, rather than directly exposing the backend error messages to the user.
A simple mapping could be:
// ERR_MSG
{
"1": "System error, please try again later!",}Copy the code
The presentation logic for message could be:
message.error(ERR_MSG[res.code])
Copy the code
Web security
Several aspects are considered, XSS, CSRF, and response headers.
XSS, refers to cross-site Scripting. The main scenario of XSS vulnerability is user input, such as comments, rich text and other information, if not verified, may be implanted malicious code, resulting in data and property loss!
Validation for XSS cannot be done by the client alone; validation must also be done by the server. I’m using [email protected] here.
npm install --save xss
Copy the code
XSS handles common XSS risks by default and is very simple to use. For example, in the new comment interface, we could do something like this for the parameter:
const xss = require("xss");
router.post('/add'.function (req, res, next) {
const params = Object.assign(req.body, {
create_time: new Date()});/ / XSS protection
if (params.content) {
params.content = xss(params.content)
}
}
Copy the code
I don’t use rich text to host comments yet, but be prepared in case I ever want to use rich text!
In the case of CSRF (cross-site request forgery) attacks, a common source of vulnerability is cookie-based authentication, because cookies are automatically carried along with HTTP requests, which allows an attacker to unknowingly trap you through script injection or some sort of seduction click. Made an unexpected request.
However, browsers are constantly improving Cookie security, such as SameSite=Lax, which is enabled by default in Chrome 80, and also protects against many CSRF attacks.
For security purposes, it is a good idea to include these attributes in set-cookies.
Set-Cookie: token=74afes7a8; HttpOnly; Secure; SameSite=Lax;
Copy the code
To prevent CSRF attacks, you can also use the CSRF-token mode or JWT authentication, which avoids the identity/password authentication based on cookies.
Also, setting up the necessary response headers is critical for Web security!
Express recommends that we use a Helmet instead.
Helmet improves security in Express applications by setting various HTTP headers. It’s not a silver bullet for Web security, but it does help!
Install the helmet:
npm install --save helmet
Copy the code
It’s also easy to use because it’s just middleware.
app.use(helmet());
Copy the code
Environment variables/configuration
Since back-end configuration files usually have some private configuration, such as database configuration, server configuration, these are not suitable for open source projects directly. Therefore, in this project, I only give an example, you can follow the instructions to give your own configuration file.
- General configuration: config/env.example.js
- The development environment configuration: config/dev. Env. Example. Js
- The production environment configuration: config/prod. Env. Example. Js
- PM2 deploy configuration: deploy.config.example.js
You are advised to configure databases, email addresses, and other parameters separately for the development environment and production environment to avoid direct impact on the production environment during local development.
Therefore, we need to set the environment identifier and reference the corresponding parameter configuration according to the environment identifier.
The environment identifier is familiar to us, which is process.env.node_env. Since the project uses PM2, I configure NODE_ENV with PM2.
env: {
NODE_ENV: "development",
PORT: 8002,
},
env_production: {
NODE_ENV: 'production',
PORT: 8002,
},
Copy the code
So, we just need to determine the development or production environment based on NODE_ENV, and then load the corresponding parameter configuration. The logic is very simple!
// Configure entry file, /env") const baseEnv = require("./env") const devEnv = require("./dev ") const prodEnv = require("./prod.env") module.exports = process.env.NODE_ENV === 'production' ? {... baseEnv, ... prodEnv } : { ... baseEnv, ... devEnv }Copy the code
summary
This article is Vue3+TS+Node to create personal blog (back-end architecture), from a not too professional perspective to cut into the back-end, mainly introduced me in the blog system design back-end some of the main ideas, many details inconvenience to expand, can open source to understand.
With the experience of full-stack development, I have greatly improved my understanding of the full link of the front and back ends, and have more topics to talk about with the backend developers. Sometimes, I can help the backend to stroke ideas and troubleshoot problems together. Very nice anyway!
However, there is still a lot of way to go to improve the back end, just look at the Java so much middleware, the road is long and long, the line is coming, come on!
series
Vue3+TS+Node to create personal blog series entry can click the link below, continue to update, welcome to read! Praise attention do not get lost! 😍
- Vue3+TS+Node to create personal blog (overview)