Javascript - это сложный язык для правильного понимания, и я устал от всех руководств, которые создают API-интерфейсы Node таким способом, который не обслуживается. Поэтому я решил создать свой собственный, основываясь на дизайне золотой структуры Php Laravel.

Описание приложения: это Restful API для Node.js и Mongo. Я также написал статью об API в Node и Mysql (SQL). Щелкните здесь, чтобы узнать об этом.

ОБНОВЛЕНИЕ. API-интерфейсы Graphql (созданные в facebook) лучше подходят для 90% приложений, чем API-интерфейсы restful. Я настоятельно рекомендую изучить graphql. Вот ссылка на шаблон, который я для них построил. "Кликните сюда"!

Это в формате MVC, за исключением того, что это API, в нем нет представлений, только модели и контроллеры. Маршрутизация: экспресс, ODM / база данных: Mongoose, Аутентификация: паспорт, JWT. Целью использования JWT (Json Web Token) является простота его интеграции с SPA (такими как Angular 2+ и React) и мобильными приложениями. Зачем создавать отдельный API для каждого, если можно создать один для обоих?

В этом руководстве предполагается, что у вас есть промежуточные знания о mongo и node. ЭТО НЕ РУКОВОДСТВО ДЛЯ НАЧИНАЮЩИХ.

Если у вас есть вопросы или предложения, я постараюсь ответить в течение часа! Я обещаю!

Начало - скачать код

Код находится на Github, и я настоятельно рекомендую сначала клонировать репо, а затем следовать. Щелкните здесь для перехода по ссылке репо. Клонируйте репо, а затем установите модули узлов.

Структура приложения

Структура использует стандартную структуру экспресс-приложения в сочетании с тем, как мангуст организует вещи, а также некоторую структуру Laravel.

— bin
— config
- - - config.js
— controllers
- - - company.controller.js
- - - home.controller.js
— — — user.controller.js
— middleware
- - - custom.js
- - - passport.js
— models
— — — index.js
— — — company.js
— — — user.js
— public
— routes
— — — v1.js
- seeders
- services
- - - auth.service.js
- - - util.service.js
.env
app.js

Давайте перейдем к Кодексу

Начнем с .env

Переименуйте example.env в .env и измените его на правильные учетные данные для вашей среды.

APP=dev
PORT=3000
DB_DIALECT=mongo
DB_HOST=localhost
DB_PORT=27017
DB_NAME=DB-Name
DB_USER=root
DB_PASSWORD=change-password
JWT_ENCRYPTION=PleaseChange
JWT_EXPIRATION=10000

Создание экземпляров переменных среды

config / config.js

Берет env из файла .env и обеспечивает стандартный способ доступа к ним во всем приложении и предоставляет им переменные по умолчанию, если переменная среды не найдена.

require('dotenv').config();//instatiate environment variables

let CONFIG = {} //Make this global to use all over the application

CONFIG.app          = process.env.APP   || 'dev';
CONFIG.port         = process.env.PORT  || '3000';

CONFIG.db_dialect   = process.env.DB_DIALECT    || 'mysql';
CONFIG.db_host      = process.env.DB_HOST       || 'localhost';
CONFIG.db_port      = process.env.DB_PORT       || '3306';
CONFIG.db_name      = process.env.DB_NAME       || 'name';
CONFIG.db_user      = process.env.DB_USER       || 'root';
CONFIG.db_password  = process.env.DB_PASSWORD   || 'db-password';

CONFIG.jwt_encryption  = process.env.JWT_ENCRYPTION || 'jwt_please_change';
CONFIG.jwt_expiration  = process.env.JWT_EXPIRATION || '10000';

module.exports = CONFIG;

Главный файл app.js

Требовать зависимости и создать экземпляр сервера.

const express      = require('express');
const logger       = require('morgan');
const bodyParser   = require('body-parser');
const passport      = require('passport');
const pe            = require('parse-error');
const cors          = require('cors');
const v1 = require('./routes/v1');

const app = express();

const CONFIG = require('./config/config');
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

//Passport
app.use(passport.initialize());

Подключиться к базе данных и загрузить модели

Это загружает файл models / index.js, который мы рассмотрим более подробно позже.

const models = require("./models");

CORS - чтобы другие веб-сайты могли отправлять запросы к этому серверу * Важно

app.use(cors());

Настройка маршрутов и обработка ошибок

app.use('/v1', v1);
app.use('/', function(req, res){
   res.statusCode = 200;//send the appropriate status code
   res.json({status:"success", message:"Parcel Pending API", data:{}})
});
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};
  // render the error page
  res.status(err.status || 500);
  res.render('error');
});
module.exports = app;

Настройте обработчик обещаний в app.js

process.on('unhandledRejection', error => {
    console.error('Uncaught Error', pe(error));
});

Сервисная служба - services / util.service.js

Это вспомогательные функции, которые мы будем использовать в приложении. Функция Кому помогает обрабатывать обещания и ошибки. Это очень полезная функция. Чтобы узнать больше о его назначении, щелкните здесь. Функции ReE и ReS помогают контроллерам отправлять ответы единообразно.

const {to} = require('await-to-js');
const pe = require('parse-error');
module.exports.to = async (promise) => {
    let err, res;
    [err, res] = await to(promise);
    if(err) return [pe(err)];
    return [null, res];
};

ReE, ReS - Стандартный способ отправки ответов

Это делается для того, чтобы каждый успешный ответ и ответ об ошибке отправлялись в одном и том же формате.

module.exports.ReE = function(res, err, code){ // Error Web Response
    if(typeof err == 'object' && typeof err.message != 'undefined'){
        err = err.message;
    }
    if(typeof code !== 'undefined') res.statusCode = code;
    return res.json({success:false, error: err});
};
module.exports.ReS = function(res, data, code){ // Success Web Response
    let send_data = {success:true};
    if(typeof data == 'object'){
        send_data = Object.assign(data, send_data);//merge the objects
    }
    if(typeof code !== 'undefined') res.statusCode = code;
    return res.json(send_data)
};

TE - это кратчайший путь к быстрому устранению ошибок.

module.exports.TE = TE = function(err_message, log){ // TE stands for Throw Error
    if(log === true){
        console.error(err_message);
    }
    throw new Error(err_message);
};

Настроить базу данных и загрузить модели.

модели / index.js

Я сказал, что мы доберемся до этого, и вот оно! Загрузите некоторые зависимости.

const fs              = require('fs');
const path            = require('path');
const basename        = path.basename(__filename);
const models          = {};
const mongoose      = require('mongoose');
const CONFIG        = require('../config/config');

подключиться к монго и загрузить модели

Загрузите все модели в каталог моделей

if(CONFIG.db_host != ''){
    var files = fs
      .readdirSync(__dirname)
      .filter((file) => {
      return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
    })
      .forEach((file) => {
        var filename = file.split('.')[0];
        var model_name = filename.charAt(0).toUpperCase() + filename.slice(1);
        models[model_name] = require('./'+file);
    });

    mongoose.Promise = global.Promise; //set mongo up to use promises
    const mongo_location = 'mongodb://'+CONFIG.db_host+':'+CONFIG.db_port+'/'+CONFIG.db_name;

    mongoose.connect(mongo_location).catch((err)=>{
        console.log('*** Can Not Connect to Mongo Server:', mongo_location)
    })

    let db = mongoose.connection;
    module.exports = db;
    db.once('open', ()=>{
        console.log('Connected to mongo at '+mongo_location);
    })
    db.on('error', (error)=>{
        console.log("error", error);
    })
    // End of Mongoose Setup
}else{
    console.log("No Mongo Credentials Given");
}

Экспорт моделей

Экспортируйте модели, чтобы получить к ним доступ во всем приложении, если нам потребуется models / index.js. Прочтите о кэшировании узлов. требуя, чтобы этот файл не загружал его каждый раз, это помогает значительно повысить производительность.

module.exports = models;

Модель пользователя

модели / user.model.js

импортные модули

const mongoose        = require('mongoose');
const bcrypt          = require('bcrypt');
const bcrypt_p        = require('bcrypt-promise');
const jwt             = require('jsonwebtoken');
const Company         = require('./company.model');
const validate        = require('mongoose-validator');
const {TE, to}        = require('../services/util.service');
const CONFIG          = require('../config/config');

Схема пользователя

let UserSchema = mongoose.Schema({
    first:      {type:String},
    last:       {type:String},
    phone:     {type:String, lowercase:true, trim: true, index: true, unique: true, sparse: true,//sparse is because now we have two possible unique keys that are optional
        validate:[validate({
            validator: 'isNumeric',
            arguments: [7, 20],
            message: 'Not a valid phone number.',
        })]
    },
    email: {type:String, lowercase:true, trim: true, index: true, unique: true, sparse: true,
            validate:[validate({
                validator: 'isEmail',
                message: 'Not a valid email.',
            }),]
    },
    password:   {type:String},

}, {timestamps: true});

Многие ко многим или один ко многим в Mongoose (все еще пользовательская модель)

сделайте ссылку на другую имеющуюся у вас модель и сохраните в ней идентификатор пользователя. Затем укажите на него виртуальный.

UserSchema.virtual('companies', {
    ref: 'Company',
    localField: '_id',
    foreignField: 'users.user',
    justOne: false,
});

Методы вспомогательной модели

Создавайте схему с помощью хуков и настраиваемых методов. Хук - это функция, которую вы можете добавить, когда что-то происходит в мангусте. Здесь мы используем его для хеширования пароля каждый раз при его изменении с помощью ловушки beforeSave.

У нас также есть собственный метод в модели для создания токена JWT для этого пользователя. Очень удобно и поддерживает многоразовый код.

UserSchema.pre('save', async function(next){

    if(this.isModified('password') || this.isNew){

        let err, salt, hash;
        [err, salt] = await to(bcrypt.genSalt(10));
        if(err) TE(err.message, true);

        [err, hash] = await to(bcrypt.hash(this.password, salt));
        if(err) TE(err.message, true);

        this.password = hash;

    } else{
        return next();
    }
})

UserSchema.methods.comparePassword = async function(pw){
    let err, pass
    if(!this.password) TE('password not set');

    [err, pass] = await to(bcrypt_p.compare(pw, this.password));
    if(err) TE(err);

    if(!pass) TE('invalid password');

    return this;
}
UserSchema.methods.Companies = async function(){
    let err, companies;
    [err, companies] = await to(Company.find({'users.user':this._id}));
    if(err) TE('err getting companies');
    return companies;
}
UserSchema.virtual('full_name').set(function (name) {
    var split = name.split(' ');
    this.first = split[0];
    this.last = split[1];
});

UserSchema.virtual('full_name').get(function () { //now you can treat as if this was a property instead of a function
    if(!this.first) return null;
    if(!this.last) return this.first;

    return this.first + ' ' + this.last;
});

UserSchema.methods.getJWT = function(){
    let expiration_time = parseInt(CONFIG.jwt_expiration);
    return "Bearer "+jwt.sign({user_id:this._id}, CONFIG.jwt_encryption, {expiresIn: expiration_time});
};
UserSchema.methods.toWeb = function(){
    let json = this.toJSON();
    json.id = this._id;//this is for the front end
    return json;
};
let User = module.exports = mongoose.model('User', UserSchema);

Модель компании

модели / company.model.js

очень простой…

const mongoose        = require('mongoose');

let CompanySchema = mongoose.Schema({
    name: {type:String},
    users:  [ {user:{type : mongoose.Schema.ObjectId, ref : 'User'}, permissions:[{type:String}]} ],
}, {timestamps: true});
CompanySchema.methods.toWeb = function(){
    let json = this.toJSON();
    json.id = this._id;//this is for the front end
    return json;
};
let company = module.exports = mongoose.model('Company', CompanySchema);

Теперь перейдем к маршрутизации нашего приложения.

маршруты / v1.js

импорт модулей и установка промежуточного программного обеспечения паспорта

const express         = require('express');
const router         = express.Router();

const UserController   = require('./../controllers/UserController');
const CompanyController = require('./../controllers/CompanyController');
const HomeController   = require('./../controllers/HomeController');

const custom           = require('./../middleware/custom');

const passport         = require('passport');
const path              = require('path');
require('./../middleware/passport')(passport)

Основные маршруты CRUD (создание, чтение, обновление, удаление). Вы можете протестировать эти маршруты с помощью почтальона или curl. В app.js мы настроили его с помощью управления версиями. Итак, чтобы сделать запрос к этим маршрутам, вы должны использовать / v1 / {route}. пример

url: localhost: 3000 / v1 / пользователи

router.post('/users',UserController.create);     
                                               
router.get('/users',passport.authenticate('jwt', {session:false}), UserController.get);
router.put('/users',passport.authenticate('jwt', {session:false}), UserController.update);
router.delete('/users',passport.authenticate('jwt',{session:false}), UserController.remove);
router.post('/users/login',UserController.login);
router.post('/companies',passport.authenticate('jwt'{session:false}), CompanyController.create);
router.get('/companies',passport.authenticate('jwt',{session:false}), CompanyController.getAll);

router.get('/companies/:company_id', passport.authenticate('jwt', {session:false}), custom.company, CompanyController.get);
router.put('/companies/:company_id', passport.authenticate('jwt', {session:false}), custom.company, CompanyController.update);

router.delete('/companies/:company_id', passport.authenticate('jwt', {session:false}), custom.company, CompanyController.remove);

router.get('/dash', passport.authenticate('jwt', {session:false}),HomeController.Dashboard)
module.exports = router;

Давайте посмотрим на промежуточное ПО, которое было быстро пропущено. (повторить код);

require('./../middleware/passport')(passport)

промежуточное ПО / паспорт.js

требуются модули

const { ExtractJwt, Strategy } = require('passport-jwt');
const { User }      = require('../models');
const CONFIG        = require('../config/config');
const {to}          = require('../services/util.service');

Это то, что определяет нашего пользователя для всех наших маршрутов с использованием промежуточного программного обеспечения паспорта. Мы храним идентификатор пользователя в токене. Затем он включается в заголовок как Authorization: Bearer a23uiabsdkjd….

Это промежуточное программное обеспечение считывает токен для идентификатора пользователя, затем захватывает пользователя и отправляет его нашим контроллерам. Я знаю, что сначала это может показаться сложным. Но использование Postman для проверки быстро обретет смысл.

module.exports = function(passport){
    var opts = {};
    opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
    opts.secretOrKey = CONFIG.jwt_encryption;

    passport.use(new Strategy(opts, async function(jwt_payload, done){
        let err, user;
        [err, user] = await to(User.findById(jwt_payload.user_id));
        if(err) return done(err, false);
        if(user) {
            return done(null, user);
        }else{
            return done(null, false);
        }
    }));
}

Пользовательское ПО промежуточного слоя

промежуточное ПО / custom.js

Это настраиваемое промежуточное программное обеспечение, поэтому мы можем должным образом убедиться, что пользователь получает доступ только к правильным моделям данных, которые им принадлежат. Это экономит время, так как отпадает необходимость повторного запроса. Также поддерживает многоразовый код.

const Company             = require('../models/company.model');
const { to, ReE, ReS }    = require('../services/util.service');

let company = async function (req, res, next) {
    let company_id, err, app;
    company_id = req.params.company_id;

    [err, company] = await to(Company.findOne({_id:company_id}));
    if(err) return ReE(res,"err finding company");

    if(!company) return ReE(res, "Company not found with id: "+company_id);
    let user, users_array;
    user = req.user;
    users_array = company.users.map(obj=>String(obj.user));

    if(!users_array.includes(String(user._id))) return ReE(res, "User does not have permission to read app with id: "+app_id);

    req.company = company;
    next();
}
module.exports.company = company;

Теперь давайте посмотрим на наши контроллеры.

контроллеры / user.controller.js

требуются модули

const { User }      = require('../models');
const authService   = require('../services/auth.service');
const { to, ReE, ReS }  = require('../services/util.service');

Создавать

Помните, что ReE - это вспомогательная функция, которая заставляет все наши сообщения об ошибках иметь один и тот же формат. Таким образом, ленивый программист не сможет испортить то, как будет выглядеть ответ. Это использует службу для фактического создания пользователя. Таким образом, наши контроллеры остаются маленькими. И ЭТО ХОРОШО!

const create = async function(req, res){
    res.setHeader('Content-Type', 'application/json');
    const body = req.body;
    if(!body.unique_key && !body.email && !body.phone){
        return ReE(res, 'Please enter an email or phone number to register.');
    } else if(!body.password){
        return ReE(res, 'Please enter a password to register.');
    }else{
        let err, user;

        [err, user] = await to(authService.createUser(body));

        if(err) return ReE(res, err, 422);
        return ReS(res, {message:'Successfully created new user.', user:user.toWeb(), token:user.getJWT()}, 201);
    }
}
module.exports.create = create;

Получить - довольно простой говорит сам за себя

пользователь возвращается в req.user из нашего промежуточного программного обеспечения для паспорта. Не забудьте включить токен в ЗАГОЛОВОК, если запрос. Авторизация: на предъявителя Jasud2732r…

const get = async function(req, res){
    res.setHeader('Content-Type', 'application/json');
    let user = req.user;

    return ReS(res, {user:user.toWeb()});
}
module.exports.get = get;

Обновление - все еще базовое

const update = async function(req, res){
    let err, user, data
    user = req.user;
    data = req.body;
    user.set(data);

    [err, user] = await to(user.save());
    if(err){
        console.log(err, user);

        if(err.message.includes('E11000')){
            if(err.message.includes('phone')){
                err = 'This phone number is already in use';
            } else if(err.message.includes('email')){
                err = 'This email address is already in use';
            }else{
                err = 'Duplicate Key Entry';
            }
        }

        return ReE(res, err);
    }
    return ReS(res, {message :'Updated User: '+user.email});
}
module.exports.update = update;

Удалять

const remove = async function(req, res){
    let user, err;
    user = req.user;

    [err, user] = await to(user.destroy());
    if(err) return ReE(res, 'error occured trying to delete user');

    return ReS(res, {message:'Deleted User'}, 204);
}
module.exports.remove = remove;

Авторизоваться

Это возвращает токен для аутентификации!

const login = async function(req, res){
    const body = req.body;
    let err, user;

    [err, user] = await to(authService.authUser(req.body));
    if(err) return ReE(res, err, 422);

    return ReS(res, {token:user.getJWT(), user:user.toWeb()});
}
module.exports.login = login;

AuthService

услуги / auth.service.js

требуются модули

const { User }      = require('../models');
const validator     = require('validator');
const { to, TE }    = require('../services/util.service');

Нам бы очень хотелось, чтобы пользователь мог использовать адрес электронной почты или номер телефона. Этот метод помогает нам объединить все, что когда-либо было отправлено, в переменную с именем unique_key. Что мы будем использовать в функции создания пользователя

const getUniqueKeyFromBody = function(body){// this is so they can send in 3 options unique_key, email, or phone and it will work
    let unique_key = body.unique_key;
    if(typeof unique_key==='undefined'){
        if(typeof body.email != 'undefined'){
            unique_key = body.email
        }else if(typeof body.phone != 'undefined'){
            unique_key = body.phone
        }else{
            unique_key = null;
        }
    }
    return unique_key;
}
module.exports.getUniqueKeyFromBody = getUniqueKeyFromBody;

Создать пользователя

Это проверяет, что уникально, чтобы увидеть, действительный ли это адрес электронной почты или действительный номер телефона. Затем сохраняет пользователя в базе данных. Довольно прохладно и довольно просто.

const createUser = async function(userInfo){
    let unique_key, auth_info, err;
    auth_info={}
    auth_info.status='create';
    unique_key = getUniqueKeyFromBody(userInfo);
    if(!unique_key) TE('An email or phone number was not entered.');
    if(validator.isEmail(unique_key)){
        auth_info.method = 'email';
        userInfo.email = unique_key;
        [err, user] = await to(User.create(userInfo));
        if(err) TE('user already exists with that email');
        return user;
    }else if(validator.isMobilePhone(unique_key, 'any')){
        auth_info.method = 'phone';
        userInfo.phone = unique_key;
        [err, user] = await to(User.create(userInfo));
        if(err) TE('user already exists with that phone number');
        return user;
    }else{
        TE('A valid email or phone number was not entered.');
    }
}
module.exports.createUser = createUser;

Пользователь аутентификации

const authUser = async function(userInfo){//returns token
    let unique_key;
    let auth_info = {};
    auth_info.status = 'login';
    unique_key = getUniqueKeyFromBody(userInfo);
    if(!unique_key) TE('Please enter an email or phone number to login');

    if(!userInfo.password) TE('Please enter a password to login');
    let user;
    if(validator.isEmail(unique_key)){
        auth_info.method='email';
        [err, user] = await to(User.findOne({email:unique_key }));
        if(err) TE(err.message);
    }else if(validator.isMobilePhone(unique_key, 'any')){
        auth_info.method='phone';
        [err, user] = await to(User.findOne({phone:unique_key }));
        if(err) TE(err.message);
    }else{
        TE('A valid email or phone number was not entered');
    }
    if(!user) TE('Not registered');
    [err, user] = await to(user.comparePassword(userInfo.password));
    if(err) TE(err.message);
    return user;
}
module.exports.authUser = authUser;

Контроллер компании

контроллеры / company.controller.js

самые простые модели имеют ту же структуру, что и пользовательский контроллер.

const { Company } = require('../models');
const { to, ReE, ReS } = require('../services/util.service');

const create = async function(req, res){
    res.setHeader('Content-Type', 'application/json');
    let err, company;
    let user = req.user;

    let company_info = req.body;
    company_info.users = [{user:user._id}];

    [err, company] = await to(Company.create(company_info));
    if(err) return ReE(res, err, 422);

    return ReS(res,{company:company.toWeb()}, 201);
}
module.exports.create = create;

const getAll = async function(req, res){
    res.setHeader('Content-Type', 'application/json');
    let user = req.user;
    let err, companies;
    [err, companies] = await to(user.Companies());

    let companies_json = []
    for (let i in companies){
        let company = companies[i];
        companies_json.push(company.toWeb())
    }
    return ReS(res, {companies: companies_json});
}
module.exports.getAll = getAll;

const get = function(req, res){
    res.setHeader('Content-Type', 'application/json');
    let company = req.company;
    return ReS(res, {company:company.toWeb()});
}
module.exports.get = get;

const update = async function(req, res){
    let err, company, data;
    company = req.user;
    data = req.body;
    company.set(data);

    [err, company] = await to(company.save());
    if(err){
        return ReE(res, err);
    }
    return ReS(res, {company:company.toWeb()});
}
module.exports.update = update;

const remove = async function(req, res){
    let company, err;
    company = req.company;

    [err, company] = await to(company.remove());
    if(err) return ReE(res, 'error occured trying to delete the company');

    return ReS(res, {message:'Deleted Company'}, 204);
}
module.exports.remove = remove;

Вот и все.

Я знаю, что не вдавался в подробности настолько, насколько мог. Пришлось пройти через многое, и код говорит сам за себя. Если у вас есть какие-либо вопросы, прокомментируйте их ниже. Я постараюсь ответить в течение часа, как я сказал.

- Брайан Алоис Шардт