Как создать систему API на основе RPC с помощью NodeJS

Node.js внешний интерфейс сервер API

Как создать систему API на основе RPC с помощью NodeJS

API съедал наши усилия по разработке в течение долгого времени, когда он существовал. Независимо от того, создаете ли вы микрослужбу, доступную только другим микрослужбам, или службу, открытую для внешнего мира, вам необходимо разработать API.

В настоящее время большинство API основаны на спецификации REST, которую легко понять и которая построена на основе протокола HTTP. Но по большей части REST может быть не для вас. Многие компании, такие как Uber, facebook, Google, netflix и т. д., создали свои собственные протоколы межсервисного взаимодействия, и ключевой вопрос здесь — когда это делать, а не нужно ли это делать.

Предположим, вы хотите использовать традиционный метод RPC, но при этом хотите передавать json-данные в формате http, как это сделать через node.js? Читайте дальше для этой статьи.

Прежде чем читать этот учебник, вы должны убедиться в следующих двух пунктах

  • У вас должен быть как минимум реальный опыт работы с Node.js.
  • Для поддержки ES6 необходимо установить Node.js.v4.0.0версия выше.

Принципы дизайна

В этом руководстве мы установим следующие два ограничения для API:

  • Будьте проще (без внешней упаковки и сложных операций)
  • Документация по API и интерфейсу, которую следует писать вместе

Начать сейчас

Полный исходный код этого руководства можно найти по адресуGithub, так что вы можете клонировать его для удобного просмотра. Во-первых, нам нужно сначала определить типы и методы, которые будут с ними работать (это будут те же самые методы, которые вызываются через API).

Создайте новый каталог и создайте два файла в новом каталоге,types.jsа такжеmethods.js. Если вы используете терминал Linux или Mac, вы можете ввести следующие команды.

mkdir noderpc && cd noderpc
touch types.js methods.js

существуетtypes.jsфайл, введите следующее.

'use strict';

let types = {
    user: {
        description:'the details of the user',
        props: {
            name:['string', 'required'],
            age: ['number'],
            email: ['string', 'required'],
            password: ['string', 'required']
        }
    },
    task: {
        description:'a task entered by the user to do at a later time',
        props: {
            userid: ['number', 'required'],
            content: ['string', 'require'],
            expire: ['date', 'required']
        }
    }
}

module.exports = types;

Простой на первый взгляд, используйтеkey-valueobject для хранения нашего типа,keyэто имя типа,valueявляется его определением. Определение включает в себя описание (фрагмент читаемого текста, в основном используемый для создания документов) и описывает каждое свойство в свойствах, поэтому дизайн в основном используется для создания и проверки документов и, наконец, передаетсяmodule.exportsнезащищенный.

существуетmethods.jsЕсть следующие.

'use strict';

let db = require('./db');

let methods = {
    createUser: {
        description: `creates a new user, and returns the details of the new user`,
        params: ['user:the user object'],
        returns: ['user'],
        exec(userObj) {
            return new Promise((resolve) => {
                if (typeof (userObj) !== 'object') {
                    throw new Error('was expecting an object!');
                }
                // you would usually do some validations here
                // and check for required fields

                // attach an id the save to db
                let _userObj = JSON.parse(JSON.stringify(userObj));
                _userObj.id = (Math.random() * 10000000) | 0; // binary or, converts the number into a 32 bit integer
                resolve(db.users.save(userObj));
            });
        }
    },

    fetchUser: {
        description: `fetches the user of the given id`,
        params: ['id:the id of the user were looking for'],
        returns: ['user'],
        exec(userObj) {
            return new Promise((resolve) => {
                if (typeof (userObj) !== 'object') {
                    throw new Error('was expecting an object!');
                }
                // you would usually do some validations here
                // and check for required fields

                // fetch
                resolve(db.users.fetch(userObj.id) || {});
            });
        }
    },

     fetchAllUsers: {
        released:false;
        description: `fetches the entire list of users`,
        params: [],
        returns: ['userscollection'],
        exec() {
            return new Promise((resolve) => {
                // fetch
                resolve(db.users.fetchAll() || {});
            });
        }
    },

};

module.exports = methods;

Как видите, по своей структуре он очень похож на модуль type, но главное отличие состоит в том, что определение каждого метода содержитexecфункция, которая возвращаетPromise. Эта функция предоставляет функциональность этого метода, хотя другие свойства также доступны пользователю, но это должно быть абстрагировано через API.

db.js

Нашему API нужно где-то хранить данные, но в этом руководстве мы не хотим передавать ненужные данные.npm installЧтобы усложнить руководство, мы создаем очень простое встроенное хранилище ключей и значений в памяти, поскольку его структура данных разработана вами, поэтому вы можете изменить способ хранения своих данных в любое время.

существуетdb.jsсодержит следующее.

'use strict';

let users = {};
let tasks = {};

// we are saving everything inmemory for now
let db = {
    users: proc(users),
    tasks: proc(tasks)
}

function clone(obj) {
    // a simple way to deep clone an object in javascript
    return JSON.parse(JSON.stringify(obj));
}

// a generalised function to handle CRUD operations
function proc(container) {
    return {
        save(obj) {
            // in JS, objects are passed by reference
            // so to avoid interfering with the original data
            // we deep clone the object, to get our own reference
            let _obj = clone(obj);

            if (!_obj.id) {
                // assign a random number as ID if none exists
                _obj.id = (Math.random() * 10000000) | 0;
            }

            container[_obj.id.toString()] = _obj;
            return clone(_obj);
        },
        fetch(id) {
            // deep clone this so that nobody modifies the db by mistake from outside
            return clone(container[id.toString()]);
        },
        fetchAll() {
            let _bunch = [];
            for (let item in container) {
                _bunch.push(clone(container[item]));
            }
            return _bunch;
        },
        unset(id) {
            delete container[id];
        }
    }
}

module.exports = db;

К числу наиболее важных относятсяprocфункция. Взяв объект и обернув его замыканием с набором функций, удобно добавлять, редактировать и удалять значения этого объекта. Если вы недостаточно знаете о замыканиях, вам следует прочитать оJavaScriptСодержимое закрытия.

Итак, теперь мы в основном завершили программную функцию, мы можем хранить и извлекать данные, а также выполнять операции над этими данными, теперь нам нужно сделать эту функцию доступной через сеть. Итак, последняя часть — реализация службы HTTP.

Именно здесь большинство из нас хочет использовать экспресс, но мы этого не делаем, поэтому мы будем использовать модуль http, который поставляется с узлом, и реализуем вокруг него очень простую таблицу маршрутизации.

Как и ожидалось, мы продолжаем создаватьserver.jsдокумент. В этом файле мы связываем все вместе, как показано ниже.

'use strict';

let http = require('http');
let url = require('url');
let methods = require('./methods');
let types = require('./types');

let server = http.createServer(requestListener);
const PORT = process.env.PORT || 9090;

Начало файла представляет то, что нам нужно, используяhttp.createServerдля создания службы HTTP.requestListenerэто функция обратного вызова, которую мы определим позже. И мы определяем порт, который сервер будет слушать.

После этого кода мы определяем таблицу маршрутизации, которая определяет различные URL-адреса, на которые будет реагировать наше приложение.

// we'll use a very very very simple routing mechanism
// don't do something like this in production, ok technically you can...
// probably could even be faster than using a routing library :-D

let routes = {
    // this is the rpc endpoint
    // every operation request will come through here
    '/rpc': function (body) {
        return new Promise((resolve, reject) => {
            if (!body) {
                throw new (`rpc request was expecting some data...!`);
            }
            let _json = JSON.parse(body); // might throw error
            let keys = Object.keys(_json);
            let promiseArr = [];

            for (let key of keys) {
                if (methods[key] && typeof (methods[key].exec) === 'function') {
                    let execPromise = methods[key].exec.call(null, _json[key]);
                    if (!(execPromise instanceof Promise)) {
                        throw new Error(`exec on ${key} did not return a promise`);
                    }
                    promiseArr.push(execPromise);
                } else {
                    let execPromise = Promise.resolve({
                        error: 'method not defined'
                    })
                    promiseArr.push(execPromise);
                }
            }

            Promise.all(promiseArr).then(iter => {
                console.log(iter);
                let response = {};
                iter.forEach((val, index) => {
                    response[keys[index]] = val;
                });

                resolve(response);
            }).catch(err => {
                reject(err);
            });
        });
    },

    // this is our docs endpoint
    // through this the clients should know
    // what methods and datatypes are available
    '/describe': function () {
        // load the type descriptions
        return new Promise(resolve => {
            let type = {};
            let method = {};

            // set types
            type = types;

            //set methods
            for(let m in methods) {
                let _m = JSON.parse(JSON.stringify(methods[m]));
                method[m] = _m;
            }

            resolve({
                types: type,
                methods: method
            });
        });
    }
};

Это очень важная часть всей программы, потому что она обеспечивает фактический интерфейс. У нас есть набор конечных точек, каждая конечная точка соответствует функции-обработчику, которая вызывается при совпадении пути. По замыслу каждая функция-обработчик должна возвращать обещание.

Конечная точка RPC получает объект json, содержащий содержимое запроса, а затем анализирует каждый запрос какmethods.jsСоответствующий метод в файле, который вызывает методexecфункцию и вернуть результат или выдать ошибку.

description просматривает конечные точки для описания методов и типов и возвращает эту информацию вызывающей стороне. Упростите для разработчиков, использующих API, понимание того, как его использовать.

Теперь давайте добавим функции, которые мы обсуждали ранее.requestListener, то вы можете запустить службу.

// request Listener
// this is what we'll feed into http.createServer
function requestListener(request, response) {
    let reqUrl = `http://${request.headers.host}${request.url}`;
    let parseUrl = url.parse(reqUrl, true);
    let pathname = parseUrl.pathname;

    // we're doing everything json
    response.setHeader('Content-Type', 'application/json');

    // buffer for incoming data
    let buf = null;

    // listen for incoming data
    request.on('data', data => {
        if (buf === null) {
            buf = data;
        } else {
            buf = buf + data;
        }
    });

    // on end proceed with compute
    request.on('end', () => {
        let body = buf !== null ? buf.toString() : null;

        if (routes[pathname]) {
            let compute = routes[pathname].call(null, body);

            if (!(compute instanceof Promise)) {
                // we're kinda expecting compute to be a promise
                // so if it isn't, just avoid it

                response.statusCode = 500;
                response.end('oops! server error!');
                console.warn(`whatever I got from rpc wasn't a Promise!`);
            } else {
                compute.then(res => {
                    response.end(JSON.stringify(res))
                }).catch(err => {
                    console.error(err);
                    response.statusCode = 500;
                    response.end('oops! server error!');
                });
            }

        } else {
            response.statusCode = 404;
            response.end(`oops! ${pathname} not found here`)
        }
    })
}

// now we can start up the server
server.listen(PORT);

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

Теперь мы можем запустить в каталогеnode server.jsчтобы запустить службу, затем используйте postman или знакомый вам инструмент отладки API, чтобыhttp://localhost{PORT}/rpcОтправьте запрос со следующим содержимым JSON в теле запроса.

{
    "createUser": {
        "name":"alloys mila",
        "age":24
    }
}

Сервер создаст нового пользователя на основе отправленного вами запроса и вернет ответ. Построена хорошо документированная система API на основе RPC.

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