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;
Вот и все.
Я знаю, что не вдавался в подробности настолько, насколько мог. Пришлось пройти через многое, и код говорит сам за себя. Если у вас есть какие-либо вопросы, прокомментируйте их ниже. Я постараюсь ответить в течение часа, как я сказал.
- Брайан Алоис Шардт