node+koa2+mongodb build a RESTful API style background

RESTful API style

Before developing, let's review what a RESTful API is? RESTful is an API design style, not a mandatory specification and standard. It is characterized by concise and clear requests and responses and strong readability. No matter what style the API belongs to, as long as it can meet the needs, it is enough. There is no absolute standard for API format, only different design styles.

API style

Generally speaking, API design consists of two parts: request and response.

  • Request: request URL, request method, request header information, etc.
  • Response: response body and response header information.

Let's first look at the composition of a request url:

https://www.baidu.com:443/api/articles?id=1
// Request method: GET
// Request protocol: protocol: HTTPS
// Request port: port: 443
// Request domain name: host: www.baidu.com com
// Request path: pathname: /api/articles
// Query string: search: id=1

According to the URL components: request method, request path and query string, we have several common API styles. For example, when deleting all articles of category 2 written by the author with id=1:

// Pure request path
GET https://www.baidu.com/api/articles/delete/authors/1/categories/2
// The first level uses the path, and the second level uses the query string
GET  https://www.baidu.com/api/articles/delete/author/1?category=2
// Pure query string
GET  https://www.baidu.com/api/deleteArticles?author=1&category=2
// RESTful style
DELETE  https://www.baidu.com/api/articles?author=1&category=2

The first three are GET requests. The main difference lies in how to pass the query string when multiple query conditions are used. Some pass the query string by using the parsing path, some pass the parameter by parsing, and some mix the two. At the same time, when describing API functions, you can use articles/delete or deleteArticles. The biggest difference of the fourth RESTful API is the position of the action verb DELETE, which is not in the url, but in the request method

RESTful design style

REST (Representational State Transfer) is a design style, not a standard. It is mainly used for the API interaction between the client and the server. I think its convention is greater than its definition, which makes the API design have certain norms and principles, and the semantics are more clear and clear.

Let's take a look at the features of RESTFul API:

  • Based on "resources", data and services are all resources in RESTFul design,

Resources are represented by URI (Universal Resource Identifier).

  • Statelessness.
  • There are usually no verbs in the URL, only nouns.
  • The semantics of URL is clear and definite.
  • Use HTTP GET, POST, DELETE and PUT to indicate the addition, deletion, modification and query of resources.
  • Use JSON instead of XML.

Take chestnuts for example, that is, the api interface to be implemented later:

GET      /api/blogs: Query article
POST     /api/blogs: New article
GET       /api/blogs/ID: Get a specified article
PUT       /api/blogs/ID: Update a specified article
DELETE   /api/blogs/ID: Delete a specified article

For more information about restful APIs, partners can: here.

Project initialization

What is Koa2

Koa official website . Official introduction: Koa is a new web framework built by the original team behind Express. It is committed to becoming a smaller, more expressive and more robust cornerstone in the field of web application and API development.

The installation and use of Koa2 is very important for node JS version is also required, because node JS 7.6 fully supports async/await, so Koa2 can be fully supported.

Koa2 is the latest version of KOA framework. Koa3 has not been officially launched yet. Koa1 X is on the way to be replaced. The biggest difference between koa2 and koa1 is that koa1 is based on co management Promise/Generator middleware, while koa2 closely follows the latest ES specification and supports Async Function (not supported by koa1). The middleware models of the two are consistent, but the underlying syntax is different.

In Express, although the code organization methods in different periods are very different, looking at the course of Express for many years, it is still a relatively large and comprehensive framework with rich API s. Its whole middleware model is based on callback callback, which has been criticized for many years.

In short, the biggest difference between Koa and Express lies in the execution sequence and asynchronous writing. At the same time, it also reflects the development of js syntax in dealing with asynchronous tasks. The difference between asynchronous and two frameworks is not discussed here. Take a look at the Koa middleware onion ring model:

Create Koa2 project

Create the file blog API and enter the directory:

npm init

Install Koa:

yarn add koa

Install eslint. You can choose to install. You can standardize your own code according to your needs. Here is my configured eslint:

yarn add eslint -D
yarn add eslint-config-airbnb-base -D
yarn add eslint-plugin-import -D

Create a new file under the root directory eslintrc.js and editorconfig:

// .eslintrc.js

module.exports = {
  root: true,
  globals: {
    document: true,
  },
  extends: 'airbnb-base',
  rules: {
    'no-underscore-dangle': 0,
    'func-names': 0,
    'no-plusplus': 0,
  },
};
// .editorconfig

root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

Create a new file app. In the root directory js:

const Koa = require('koa');

const app = new Koa();

app.use(async (ctx) => {
  ctx.body = 'Hello World';
});

app.listen(3000);

Start the project by command:

node app.js

Open in browser http://localhost:3000/:

Project development

directory structure

Plan the project structure and create corresponding folders:

blog-api
├── bin    // Project startup document
├── config   // Project profile
├── controllers    // controller
├── dbhelper    // Database operation
├── error    // error handling
├── middleware    // middleware 
├── models    // database model
├── node_modules  
├── routers    // route
├── util    // Tool class
├── README.md    // Documentation
├── package.json
├── app.js    // Entry file
└── yarn.lock

Automatic restart

After writing the debugging project and modifying the code, you need to close it manually and restart it frequently, which is very cumbersome. Install the automatic restart tool nodemon:

yarn add nodemon -D

Then install cross Env, which is mainly used to set the compatibility of environment variables:

yarn add cross-env

In package Add scripts to the scripts of JSON:

{
  "name": "blog-api",
  "version": "1.0.0",
  "description": "Personal blog background api",
  "main": "app.js",
  "scripts": {
    "dev": "cross-env NODE_ENV=development nodemon ./app.js",
    "rc": "cross-env NODE_ENV=production nodemon ./app.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Mingme <419654548@qq.com>",
  "license": "ISC",
  "dependencies": {
    "cross-env": "^7.0.2",
    "koa": "^2.13.0",
    "koa-router": "^10.0.0"
  },
  "devDependencies": {
    "eslint": "^7.13.0",
    "eslint-config-airbnb-base": "^14.2.1",
    "eslint-plugin-import": "^2.22.1",
    "nodemon": "^2.0.6"
  }
}

At this time, we can run the project through the script we set, and the modified file will restart automatically after saving.

Run in production mode

yarn rc
// perhaps
npm run rc

Run in development mode

yarn dev
// perhaps
npm run dev

koa routing

Routing is composed of a URI (or path) and a specific HTTP method (GET, POST, etc.), which involves how the application responds to the client's access to a website node.

yarn add koa-router

The interface is uniformly prefixed with / api, for example:

http://localhost:3000/api/categories
http://localhost:3000/api/blogs

Create index. In the config directory js:

// config/index.js
module.exports = {
  apiPrefix: '/api',
};

Create index. In the routers directory js , category. js , blog. js :

// routers/category.js
const router = require('koa-router')();

router.get('/', async (ctx) => {
  // ctx context, which contains information such as request and response
  ctx.body = 'I'm the classification interface';
});

module.exports = router;
// routers/blog.js
const router = require('koa-router')();

router.get('/', async (ctx) => {
  ctx.body = 'I am the article interface';
});

module.exports = router;
// routers/index.js
const router = require('koa-router')();
const { apiPrefix } = require('../config/index');

const blog = require('./blog');
const category = require('./category');

router.prefix(apiPrefix);

router.use('/blogs', blog.routes(), blog.allowedMethods());
router.use('/categories', category.routes(), category.allowedMethods());

module.exports = router;

On app Modify the code in JS and introduce the route:

// app.js
const Koa = require('koa');

const app = new Koa();

const routers = require('./routers/index');

// routers
app.use(routers.routes()).use(routers.allowedMethods());

app.listen(3000);

Start the project locally and see the effect:


Different contents are displayed according to different routes, indicating that the route is OK.

GET request

Next, let's take a look at parameter passing. If the request id is 1, our GET request is generally written as follows:

http://localhost:3000/api/blogs/1
http://localhost:3000/api/blogs?id=1
// routers/blog.js
const router = require('koa-router')();

router.get('/', async (ctx) => {
  /**
    In koa2, GET passes the value and receives it through request, but there are two receiving methods: query and querystring.
    query: Returns a formatted parameter object.
    querystring: The request string is returned.
  */
  ctx.body = `Interface is my article id: ${ctx.query.id}`;
});

// Dynamic routing
router.get('/:id', async (ctx) => {
  ctx.body = `Dynamic routing article interface id: ${ctx.params.id}`;
});

module.exports = router;

As shown in the figure:

POST/PUT/DEL

GET includes the parameters in the URL, and POST passes the parameters through request body.
To facilitate the use of KOA body to handle POST requests and file uploads, you can also use koa body parser and KOA multer.

yarn add koa-body

To unify the data format and make the data JSON, install koa JSON:

yarn add koa-json

Use koa logger to facilitate debugging:

yarn add koa-logger

On app Introducing middleware into JS:

const Koa = require('koa');

const path = require('path');

const app = new Koa();
const koaBody = require('koa-body');
const json = require('koa-json');
const logger = require('koa-logger');

const routers = require('./routers/index');

// middlewares
app.use(koaBody({
  multipart: true, // Support file upload
  formidable: {
    formidable: {
      uploadDir: path.join(__dirname, 'public/upload/'), // Set file upload directory
      keepExtensions: true, // Maintain file suffix
      maxFieldsSize: 2 * 1024 * 1024, // File upload size
      onFileBegin: (name, file) => { // Settings before file upload
        console.log(`name: ${name}`);
        console.log(file);
      },
    },
  },
}));
app.use(json());
app.use(logger());

// routers
app.use(routers.routes()).use(routers.allowedMethods());

app.listen(3000);

At routes / blog Add route under JS:

// routers/blog.js

const router = require('koa-router')();

router.get('/', async (ctx) => {
  ctx.body = `Interface is my article id: ${ctx.query.id}`;
});

// Dynamic routing
router.get('/:id', async (ctx) => {
  ctx.body = `Dynamic routing article interface id: ${ctx.params.id}`;
});

router.post('/', async (ctx) => {
  ctx.body = ctx.request.body;
});

router.put('/:id', async (ctx) => {
  ctx.body = `PUT: ${ctx.params.id}`;
});

router.del('/:id', async (ctx) => {
  ctx.body = `DEL: ${ctx.params.id}`;
});

module.exports = router;

Test:


error handling

During the request process, you also need to wrap the returned results. When an exception occurs, if there is no prompt on the interface, the return of the status code must be unfriendly. Several common error types are defined below.
Create API in error directory_ error_ map. js , api_error_name.js , api_error.js:

// error/api_error_map.js

const ApiErrorNames = require('./api_error_name');

const ApiErrorMap = new Map();

ApiErrorMap.set(ApiErrorNames.NOT_FOUND, { code: ApiErrorNames.NOT_FOUND, message: 'The interface was not found' });
ApiErrorMap.set(ApiErrorNames.UNKNOW_ERROR, { code: ApiErrorNames.UNKNOW_ERROR, message: 'unknown error' });
ApiErrorMap.set(ApiErrorNames.LEGAL_ID, { code: ApiErrorNames.LEGAL_ID, message: 'id wrongful' });
ApiErrorMap.set(ApiErrorNames.UNEXIST_ID, { code: ApiErrorNames.UNEXIST_ID, message: 'id non-existent' });
ApiErrorMap.set(ApiErrorNames.LEGAL_FILE_TYPE, { code: ApiErrorNames.LEGAL_FILE_TYPE, message: 'File type not allowed' });
ApiErrorMap.set(ApiErrorNames.NO_AUTH, { code: ApiErrorNames.NO_AUTH, message: 'No operation permission' });

module.exports = ApiErrorMap;
// error/api_error_name.js

const ApiErrorNames = {
  NOT_FOUND: 'not_found',
  UNKNOW_ERROR: 'unknow_error',
  LEGAL_ID: 'legal_id',
  UNEXIST_ID: 'unexist_id',
  LEGAL_FILE_TYPE: 'legal_file_type',
  NO_AUTH: 'no_auth',
};

module.exports = ApiErrorNames;
// error/api_error.js

const ApiErrorMap = require('./api_error_map');

/**
 * Custom Api exception
 */

class ApiError extends Error {
  constructor(errorName, errorMsg) {
    super();

    let errorInfo = {};
    if (errorMsg) {
      errorInfo = {
        code: errorName,
        message: errorMsg,
      };
    } else {
      errorInfo = ApiErrorMap.get(errorName);
    }

    this.name = errorName;
    this.code = errorInfo.code;
    this.message = errorInfo.message;
  }
}

module.exports = ApiError;

Create a response in the middleware directory_ formatter. JS is used to handle the formatting of the data returned by the api:

// middleware/response_formatter.js

const ApiError = require('../error/api_error');
const ApiErrorNames = require('../error/api_error_name');

const responseFormatter = (apiPrefix) => async (ctx, next) => {
  if (ctx.request.path.startsWith(apiPrefix)) {
    try {
      // Run the route first
      await next();

      if (ctx.response.status === 404) {
        throw new ApiError(ApiErrorNames.NOT_FOUND);
      } else {
        ctx.body = {
          code: 'success',
          message: 'success!',
          result: ctx.body,
        };
      }
    } catch (error) {
      // If the exception type is API exception, the error information is added to the response body and returned.
      if (error instanceof ApiError) {
        ctx.body = {
          code: error.code,
          message: error.message,
        };
      } else {
        ctx.status = 400;
        ctx.response.body = {
          code: error.name,
          message: error.message,
        };
      }
    }
  } else {
    await next();
  }
};

module.exports = responseFormatter;

Install koa's error handler hack:

yarn add koa-onerror

On app Add code to JS:

const Koa = require('koa');

const path = require('path');

const app = new Koa();
const onerror = require('koa-onerror');
const koaBody = require('koa-body');
const json = require('koa-json');
const logger = require('koa-logger');

const responseFormatter = require('./middleware/response_formatter');
const { apiPrefix } = require('./config/index');
const routers = require('./routers/index');

// koa's error handler hack
onerror(app);

// middlewares
app.use(koaBody({
  multipart: true, // Support file upload
  formidable: {
    formidable: {
      uploadDir: path.join(__dirname, 'public/upload/'), // Set file upload directory
      keepExtensions: true, // Maintain file suffix
      maxFieldsSize: 2 * 1024 * 1024, // File upload size
      onFileBegin: (name, file) => { // Settings before file upload
        console.log(`name: ${name}`);
        console.log(file);
      },
    },
  },
}));
app.use(json());
app.use(logger());

// response formatter
app.use(responseFormatter(apiPrefix));

// routers
app.use(routers.routes()).use(routers.allowedMethods());

// Listening error
app.on('error', (err, ctx) => {
  // Here you can process the error information and generate logs.
  console.error('server error', err, ctx);
});

app.listen(3000);

In subsequent development, if an exception is encountered, the exception can be thrown.

Connect database

Installation tutorial of mongoDB database: Installation and configuration of mongodb+node for Linux server (CentOS).

mongoose : nodeJS provides a library to connect mongodb.

mongoose-paginate : mongoose's paging plug-in.

mongoose-unique-validator : you can add pre saved validation for unique fields in Mongoose schema.

yarn add mongoose
yarn add mongoose-paginate
yarn add mongoose-unique-validator

In config / index Add configuration in JS:

module.exports = {
  port: process.env.PORT || 3000,
  apiPrefix: '/api',
  database: 'mongodb://localhost:27017/test',
  databasePro: 'mongodb://root:123456@110.110.110.110:27017/blog', // mongodb: / / user name: password @ server public IP: port / library name
};

Create dB. In the dbhelper directory js:

const mongoose = require('mongoose');
const config = require('../config');

mongoose.Promise = global.Promise;

const IS_PROD = ['production', 'prod', 'pro'].includes(process.env.NODE_ENV);
const databaseUrl = IS_PROD ? config.databasePro : config.database;

/**
 *  Connect database
 */

mongoose.connect(databaseUrl, {
  useUnifiedTopology: true,
  useNewUrlParser: true,
  useFindAndModify: false,
  useCreateIndex: true,
  config: {
    autoIndex: false,
  },
});

/**
 *  Connection successful
 */

mongoose.connection.on('connected', () => {
  console.log(`Mongoose Connection successful: ${databaseUrl}`);
});

/**
 *  Connection exception
 */

mongoose.connection.on('error', (err) => {
  console.log(`Mongoose link error : ${err}`);
});

/**
 *  Disconnected
 */

mongoose.connection.on('disconnected', () => {
  console.log('Mongoose Connection closed!');
});

module.exports = mongoose;

On app JS:

...
const routers = require('./routers/index');

require('./dbhelper/db');

// koa's error handler hack
onerror(app);
...

After starting the project, you can see the log prompt that the connection is successful:

Let's talk about dB JS has such a line of code:

mongoose.Promise = global.Promise;

This is added because all query operations of mongoose return query results. An object encapsulated by mongoose is not a complete promise, and it is different from the promise of ES6 standard. Therefore, this sentence is generally added when using mongoose Promise = global. Promise.

Development API

Everything about Mongoose starts with Schema. Before developing the interface, let's build the model first. Here, we mainly build two types of interfaces: article classification and article list. The fields will be relatively simple, which are mainly used for examples. Partners can draw inferences from one example.
Create category. In the models directory JS and blog js:

// models/category.js

const mongoose = require('mongoose');
const mongoosePaginate = require('mongoose-paginate');
const uniqueValidator = require('mongoose-unique-validator');

const schema = new mongoose.Schema({
  name: {
    type: String,
    unique: true,
    required: [true, 'classification name Required'],
  },
  value: {
    type: String,
    unique: true,
    required: [true, 'classification value Required'],
  },
  rank: {
    type: Number,
    default: 0,
  },
}, {
  timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
});

// Automatically increase version number
/* Mongoose Update the version key save() only when you use it. If you use update(), findOneAndUpdate(), and so on, Mongoose will not update the version key.
As a solution, you can use the following middleware. reference resources https://mongoosejs.com/docs/guide.html#versionKey */

schema.pre('findOneAndUpdate', function () {
  const update = this.getUpdate();
  if (update.__v != null) {
    delete update.__v;
  }
  const keys = ['$set', '$setOnInsert'];
  Object.keys(keys).forEach((key) => {
    if (update[key] != null && update[key].__v != null) {
      delete update[key].__v;
      if (Object.keys(update[key]).length === 0) {
        delete update[key];
      }
    }
  });
  update.$inc = update.$inc || {};
  update.$inc.__v = 1;
});

schema.plugin(mongoosePaginate);
schema.plugin(uniqueValidator);

module.exports = mongoose.model('Category', schema);
// models/blog.js

const mongoose = require('mongoose');
const uniqueValidator = require('mongoose-unique-validator');
const mongoosePaginate = require('mongoose-paginate');

const schema = new mongoose.Schema({
  title: {
    type: String,
    unique: true,
    required: [true, 'Mandatory Field '],
  }, // title
  content: {
    type: String,
    required: [true, 'Mandatory Field '],
  }, // content
  category: {
    type: mongoose.Schema.Types.ObjectId,
    required: [true, 'Mandatory Field '],
    ref: 'Category',
  }, // Classification_ id, according to which we can find relevant data from the category table.
  status: {
    type: Boolean,
    default: true,
  }, // state
}, {
  timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' },
  toJSON: { virtuals: true },
});

// According to: virtual field_ id to find the data in the corresponding table.
schema.virtual('categoryObj', {
  ref: 'Category',
  localField: 'category',
  foreignField: '_id',
  justOne: true,
});

// Automatically increase version number
/* Mongoose Update the version key save() only when you use it. If you use update(), findOneAndUpdate(), and so on, Mongoose will not update the version key.
As a solution, you can use the following middleware. reference resources https://mongoosejs.com/docs/guide.html#versionKey */

schema.pre('findOneAndUpdate', function () {
  const update = this.getUpdate();
  if (update.__v != null) {
    delete update.__v;
  }
  const keys = ['$set', '$setOnInsert'];
  Object.keys(keys).forEach((key) => {
    if (update[key] != null && update[key].__v != null) {
      delete update[key].__v;
      if (Object.keys(update[key]).length === 0) {
        delete update[key];
      }
    }
  });
  update.$inc = update.$inc || {};
  update.$inc.__v = 1;
});

schema.plugin(mongoosePaginate);
schema.plugin(uniqueValidator);

module.exports = mongoose.model('Blog', schema);

In the dbhelper directory, define some methods to add, delete, modify and query the database, and create category JS and blog js:

// dbhelper/category.js

const Model = require('../models/category');

// TODO: Promise is best returned in this file. Pass exec() can return Promise.
// It should be noted that the paging plug-in itself returns Promise, so model Paginate does not require exec().
// Model.create also returns Promise

/**
 * Find all
 */
exports.findAll = () => Model.find().sort({ rank: 1 }).exec();

/**
 * Find multiple filters
 */
exports.findSome = (data) => {
  const {
    page = 1, limit = 10, sort = 'rank',
  } = data;
  const query = {};
  const options = {
    page: parseInt(page, 10),
    limit: parseInt(limit, 10),
    sort,
  };
  const result = Model.paginate(query, options);

  return result;
};

/**
 * Find individual details
 */
exports.findById = (id) => Model.findById(id).exec();

/**
 * increase
 */
exports.add = (data) => Model.create(data);

/**
 * to update
 */
exports.update = (data) => {
  const { id, ...restData } = data;
  return Model.findOneAndUpdate({ _id: id }, {
    ...restData,
  },
  {
    new: true, // Modified data returned
  }).exec();
};

/**
 * delete
 */
exports.delete = (id) => Model.findByIdAndDelete(id).exec();
// dbhelper/blog.js

const Model = require('../models/blog');

// TODO: Promise is best returned in this file. Pass exec() can return Promise.
// It should be noted that the paging plug-in itself returns Promise, so model Paginate does not require exec().
// Model.create also returns Promise

const populateObj = [
  {
    path: 'categoryObj',
    select: 'name value',
  },
];

/**
 * Find all
 */
exports.findAll = () => Model.find().populate(populateObj).exec();

/**
 * Find multiple filters
 */
exports.findSome = (data) => {
  const {
    keyword, title, category, status = true, page = 1, limit = 10, sort = '-createdAt',
  } = data;
  const query = {};
  const options = {
    page: parseInt(page, 10),
    limit: parseInt(limit, 10),
    sort,
    populate: populateObj,
  };

  if (status !== 'all') {
    query.status = status === true || status === 'true';
  }

  if (title) {
    query.title = { $regex: new RegExp(title, 'i') };
  }

  if (category) {
    query.category = category;
  }

  // Keyword fuzzy query title and content
  if (keyword) {
    const reg = new RegExp(keyword, 'i');
    const fuzzyQueryArray = [{ content: { $regex: reg } }];
    if (!title) {
      fuzzyQueryArray.push({ title: { $regex: reg } });
    }
    query.$or = fuzzyQueryArray;
  }

  return Model.paginate(query, options);
};

/**
 * Find individual details
 */
exports.findById = (id) => Model.findById(id).populate(populateObj).exec();

/**
 * newly added
 */
exports.add = (data) => Model.create(data);

/**
 * to update
 */
exports.update = (data) => {
  const { id, ...restData } = data;
  return Model.findOneAndUpdate({ _id: id }, {
    ...restData,
  }, {
    new: true, // Return modified data
  }).exec();
};

/**
 * delete
 */
exports.delete = (id) => Model.findByIdAndDelete(id).exec();

Write route:

// routers/category.js

const router = require('koa-router')();
const controller = require('../controllers/category');

// check
router.get('/', controller.find);

// Check dynamic routing
router.get('/:id', controller.detail);

// increase
router.post('/', controller.add);

// change
router.put('/:id', controller.update);

// Delete
router.del('/:id', controller.delete);

module.exports = router;
// routers/blog.js

const router = require('koa-router')();
const controller = require('../controllers/blog');

// check
router.get('/', controller.find);

// Check dynamic routing
router.get('/:id', controller.detail);

// increase
router.post('/', controller.add);

// change
router.put('/:id', controller.update);

// Delete
router.del('/:id', controller.delete);

module.exports = router;

In the routing file, we only define the route and put all the methods corresponding to the route under controllers:

// controllers/category.js
const dbHelper = require('../dbhelper/category');
const tool = require('../util/tool');

const ApiError = require('../error/api_error');
const ApiErrorNames = require('../error/api_error_name');

/**
 * check
 */
exports.find = async (ctx) => {
  let result;
  const reqQuery = ctx.query;

  if (reqQuery && !tool.isEmptyObject(reqQuery)) {
    if (reqQuery.id) {
      result = dbHelper.findById(reqQuery.id);
    } else {
      result = dbHelper.findSome(reqQuery);
    }
  } else {
    result = dbHelper.findAll();
  }

  await result.then((res) => {
    if (res) {
      ctx.body = res;
    } else {
      throw new ApiError(ApiErrorNames.UNEXIST_ID);
    }
  }).catch((err) => {
    throw new ApiError(err.name, err.message);
  });
};

/**
 * Check dynamic routing id
 */
exports.detail = async (ctx) => {
  const { id } = ctx.params;
  if (!tool.validatorsFun.numberAndCharacter(id)) {
    throw new ApiError(ApiErrorNames.LEGAL_ID);
  }
  await dbHelper.findById(id).then((res) => {
    if (res) {
      ctx.body = res;
    } else {
      throw new ApiError(ApiErrorNames.UNEXIST_ID);
    }
  }).catch((err) => {
    throw new ApiError(err.name, err.message);
  });
};

/**
 * add to
 */
exports.add = async (ctx) => {
  const dataObj = ctx.request.body;

  await dbHelper.add(dataObj).then((res) => {
    ctx.body = res;
  }).catch((err) => {
    throw new ApiError(err.name, err.message);
  });
};

/**
 * to update
 */
exports.update = async (ctx) => {
  const ctxParams = ctx.params;
  // Merge the parameters in the route and the parameters sent
  // Both the routing parameters and the parameters sent may have IDs. The id sent shall prevail. If not, the id in the route shall be taken
  const dataObj = { ...ctxParams, ...ctx.request.body };

  await dbHelper.update(dataObj).then((res) => {
    if (res) {
      ctx.body = res;
    } else {
      throw new ApiError(ApiErrorNames.UNEXIST_ID);
    }
  }).catch((err) => {
    throw new ApiError(err.name, err.message);
  });
};

/**
 * delete
 */
exports.delete = async (ctx) => {
  const ctxParams = ctx.params;
  // Merge the parameters in the route and the parameters sent
  // Both the routing parameters and the parameters sent may have IDs. The id sent shall prevail. If not, the id in the route shall be taken
  const dataObj = { ...ctxParams, ...ctx.request.body };
  if (!tool.validatorsFun.numberAndCharacter(dataObj.id)) {
    throw new ApiError(ApiErrorNames.LEGAL_ID);
  }

  await dbHelper.delete(dataObj.id).then((res) => {
    if (res) {
      ctx.body = res;
    } else {
      throw new ApiError(ApiErrorNames.UNEXIST_ID);
    }
  }).catch((err) => {
    throw new ApiError(err.name, err.message);
  });
};
// controllers/blog.js
const dbHelper = require('../dbhelper/blog');
const tool = require('../util/tool');

const ApiError = require('../error/api_error');
const ApiErrorNames = require('../error/api_error_name');

/**
 * check
 */
exports.find = async (ctx) => {
  let result;
  const reqQuery = ctx.query;

  if (reqQuery && !tool.isEmptyObject(reqQuery)) {
    if (reqQuery.id) {
      result = dbHelper.findById(reqQuery.id);
    } else {
      result = dbHelper.findSome(reqQuery);
    }
  } else {
    result = dbHelper.findAll();
  }

  await result.then((res) => {
    if (res) {
      ctx.body = res;
    } else {
      throw new ApiError(ApiErrorNames.UNEXIST_ID);
    }
  }).catch((err) => {
    throw new ApiError(err.name, err.message);
  });
};

/**
 * Check details
 */
exports.detail = async (ctx) => {
  const { id } = ctx.params;
  if (!tool.validatorsFun.numberAndCharacter(id)) {
    throw new ApiError(ApiErrorNames.LEGAL_ID);
  }

  await dbHelper.findById(id).then(async (res) => {
    if (res) {
      ctx.body = res;
    } else {
      throw new ApiError(ApiErrorNames.UNEXIST_ID);
    }
  }).catch((err) => {
    throw new ApiError(err.name, err.message);
  });
};

/**
 * increase
 */
exports.add = async (ctx) => {
  const dataObj = ctx.request.body;

  await dbHelper.add(dataObj).then((res) => {
    ctx.body = res;
  }).catch((err) => {
    throw new ApiError(err.name, err.message);
  });
};

/**
 * change
 */
exports.update = async (ctx) => {
  const ctxParams = ctx.params;
  // Merge the parameters in the route and the parameters sent
  // Both the routing parameters and the parameters sent may have IDs. The id sent shall prevail. If not, the id in the route shall be taken
  const dataObj = { ...ctxParams, ...ctx.request.body };

  await dbHelper.update(dataObj).then((res) => {
    if (res) {
      ctx.body = res;
    } else {
      throw new ApiError(ApiErrorNames.UNEXIST_ID);
    }
  }).catch((err) => {
    throw new ApiError(err.name, err.message);
  });
};

/**
 * Delete
 */
exports.delete = async (ctx) => {
  const ctxParams = ctx.params;
  // Merge the parameters in the route and the parameters sent
  // Both the routing parameters and the parameters sent may have IDs. The id sent shall prevail. If not, the id in the route shall be taken
  const dataObj = { ...ctxParams, ...ctx.request.body };
  if (!tool.validatorsFun.numberAndCharacter(dataObj.id)) {
    throw new ApiError(ApiErrorNames.LEGAL_ID);
  }

  await dbHelper.delete(dataObj.id).then((res) => {
    if (res) {
      ctx.body = res;
    } else {
      throw new ApiError(ApiErrorNames.UNEXIST_ID);
    }
  }).catch((err) => {
    throw new ApiError(err.name, err.message);
  });
};

The above two methods are used. isEmptyObject determines whether it is an empty object, and numberAndCharacter makes a simple check on the id format.

// util/tool.js
/**
 * @desc Check if it is an empty object
 */
exports.isEmptyObject = (obj) => Object.keys(obj).length === 0;

/**
 * @desc Regular regular check expression
 */
exports.validatorsExp = {
  number: /^[0-9]*$/,
  numberAndCharacter: /^[0-9a-zA-Z]+$/,
  nameLength: (n) => new RegExp(`^[\\u4E00-\\u9FA5]{${n},}$`),
  idCard: /^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)$/,
  backCard: /^([1-9]{1})(\d{15}|\d{18})$/,
  phone: /^1[3456789]\d{9}$/,
  email: /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/,
};

/**
 * @desc Regular check method
 */
exports.validatorsFun = {
  number: (val) => exports.validatorsExp.number.test(val),
  numberAndCharacter: (val) => exports.validatorsExp.numberAndCharacter.test(val),
  idCard: (val) => exports.validatorsExp.idCard.test(val),
  backCard: (val) => exports.validatorsExp.backCard.test(val),
};

So far, the interfaces related to classification and articles have been basically completed. Test:


authentication

Here I use token for authentication: jsonwebtoken
Perform token verification on some interfaces of non GET requests according to the route.

// app.js
...
// Check whether the token expires when requesting
app.use(tokenHelper.checkToken([
  '/api/blogs',
  '/api/categories',
  ...
], [
  '/api/users/signup',
  '/api/users/signin',
  '/api/users/forgetPwd',
]));
...
// util/token-helper.js

const jwt = require('jsonwebtoken');
const config = require('../config/index');
const tool = require('./tool');

// Generate token
exports.createToken = (user) => {
  const token = jwt.sign({ userId: user._id, userName: user.userName }, config.tokenSecret, { expiresIn: '2h' });
  return token;
};

// The decryption token returns userid, which is used to judge the user's identity.
exports.decodeToken = (ctx) => {
  const token = tool.getTokenFromCtx(ctx);
  const userObj = jwt.decode(token, config.tokenSecret);
  return userObj;
};

// Check token
exports.checkToken = (shouldCheckPathArray, unlessCheckPathArray) => async (ctx, next) => {
  const currentUrl = ctx.request.url;
  const { method } = ctx.request;

  const unlessCheck = unlessCheckPathArray.some((url) => currentUrl.indexOf(url) > -1);

  const shouldCheck = shouldCheckPathArray.some((url) => currentUrl.indexOf(url) > -1) && method !== 'GET';

  if (shouldCheck && !unlessCheck) {
    const token = tool.getTokenFromCtx(ctx);
    if (token) {
      try {
        jwt.verify(token, config.tokenSecret);
        await next();
      } catch (error) {
        ctx.status = 401;
        ctx.body = 'token be overdue';
      }
    } else {
      ctx.status = 401;
      ctx.body = 'nothing token,Please login';
    }
  } else {
    await next();
  }
};

Generate setting token when registering a login:

// controllers/users.js

/**
 * @desc register
 */
 ...
exports.signUp = async (ctx) => {
  const dataObj = ctx.request.body;

  await dbHelper.signUp(dataObj).then((res) => {
    const token = tokenHelper.createToken(res);
    const { password, ...restData } = res._doc;
    ctx.res.setHeader('Authorization', token);
    ctx.body = {
      token,
      ...restData,
    };
  }).catch((err) => {
    throw new ApiError(err.name, err.message);
  });
};

/**
 * @desc Sign in
 */
exports.signIn = async (ctx) => {
  const dataObj = ctx.request.body;

  await dbHelper.signIn(dataObj).then((res) => {
    const token = tokenHelper.createToken(res);
    const { password, ...restData } = res;
    ctx.res.setHeader('Authorization', token);
    ctx.body = {
      token,
      ...restData,
    };
  }).catch((err) => {
    throw new ApiError(err.name, err.message);
  });
};
...

Project deployment

The deployment is relatively simple. Upload all the project files to the server, then install pm2 globally and start it with pm2.

Create pm2.0 in bin directory config. json :

{
  "apps": [
    {
      "name": "blog-api",
      "script": "./app.js",
      "instances": 0,
      "watch": false,
      "exec_mode": "cluster_mode"
    }
  ]
}

In package Add a startup script to JSON:

{
  ...
  "scripts": {
    "dev": "cross-env NODE_ENV=development nodemon ./app.js",
    "rc": "cross-env NODE_ENV=production nodemon ./app.js",
    "pm2": "cross-env NODE_ENV=production NODE_LOG_DIR=/tmp ENABLE_NODE_LOG=YES pm2 start ./bin/pm2.config.json",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
 ...
}

Then, cd to the project root directory:

npm run pm2

For personal blog foreground development, you can stamp here: Nuxt development blog

Tags: Javascript node.js MongoDB api koa2

Posted by powlouk on Mon, 02 May 2022 17:19:39 +0300