npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

cluer

v0.0.3

Published

Be simple, but dynamic! Cluer is an api aggregator with a low learning curve!

Downloads

6

Readme

Motivation: have you ever seen yourself needing to compose a response to your front-end coming from multiple backends? It can be a pain, not to mention that sometimes you don't need all the data provided from your backend APIs. For that, we have many different API gateways and solutions that are not always easy to use, and not as simple as we need. Cluer was born to simplify this whole process. It's fast, easy to configurate, made with pure Javascript and totally customizable.

This is yet an experimental project. Feel free to use in production, but with caution. Colaborations are always welcome. Any issues that you have, feel free to share with me. This project was made to be simple. If you need a more complex solution, you may try some others alternatives. Believe, there are plenty of them.

In examples folder you will find many use cases to apply in your APIs. Everything that Cluer can do is in there, so don't be afraid of spending some time looking at it. The idea is to keep all these examples updated, but things can go wrong. If you don't find what you need, feel free to ask.

First, add it to your project by running yarn or npm.

yarn add cluer

Then import cluer to your project

const cluer = require('cluer');

Cluer uses Express JS behind the scene, so what we need to do is to provide a configuration file and alternatively provide a PORT to startup your api. Simple as that:

const endpoints = require('endpoints.json');

cluer.init(endpoints, {
    port: 3000
});

Great, so now we are just one step away of getting our api up and running. We need now to create the endpoints configuration file. The following lines have a short example of how to do that.

{
    "version": 1,
    "name": "Hello Cluer World",
    "endpoints": [
        {
            "path": "/api/aggregate",
            "method": "GET",
            "backends": [
                {
                    "key": "account",
                    "host": "http://www.mocky.io",
                    "path": "/v2/5d010e843200000d00f9da23",
                    "method": "GET"
                },
                {
                    "key": "videos",
                    "host": "http://www.mocky.io",
                    "path": "/v2/5cffad0c3200003800eac8bc",
                    "method": "GET"
                },
                {
                    "key": "permissions",
                    "host": "http://www.mocky.io",
                    "path": "/v2/5cffad193200000d00eac8bd",
                    "method": "GET"
                }
            ],
            "response": {
                "root": [ "$account", "$videos", "$permissions" ]
            }
        }
    ]
}

And you're ready to go!

As Cluer uses ExpressJS behind the scenes, parameters and querystring are most like the same. You can specify parameters by adding it directly to your route:

{
    "endpoints": [{
        "path": "/api/aggregate/:videoId",
        "backends": [ ... ],
        "response": { ... }
    }]
}

Querystrings are captured when a querystring object is specified. Let's see how to use it:

{
    "endpoints": [{
        "path": "/api/aggregate",
        "querystring": [ "videoId" ],
        "backends": [{
            "key": "videos",
            "host": "http://www.mocky.io",
            "path": "/v2/:videoId"
        }],
        "response": { ... }
    }]
}

Note that we are receiving a value from a querystring called videoId, and passing as a parameter to one of our backend routes. But what if you want to receive a value in your path and pass as a querystring to your backend? Let's see how it goes:

{
    "endpoints": [{
        "path": "/api/aggregate/:videoId",
        "backends": [{
            "key": "videos",
            "host": "http://www.mocky.io",
            "path": "/v2/5cffad193200000d00eac8bd",
            "querystring": [ "videoId" ]
        }],
        "response": { ... }
    }]
}

Cluer can receive and forward payloads to your backends. It's easy and accessible using the keyword "$body". Working with $body is pretty much the same as handling responses. You can use JSONPath to resolve and translate your data.

{
    "endpoints": [{
        "path": "/api/aggregate/:videoId",
        "method": "POST",
        "backends": [{
            "key": "videos",
            "host": "http://www.mocky.io",
            "path": "/v2/5cffad193200000d00eac8bd",
            "method": "POST",
            "body": "$body"
        }],
        "response": { ... }
    }]
}

The above example shows how to pass your entire $body to a single backend. But what if we wanted to break our $body into two pieces?

{
    "endpoints": [{
        "path": "/api/aggregate/:videoId",
        "method": "POST",
        "backends": [
            {
                "key": "videos",
                "host": "http://www.mocky.io",
                "path": "/v2/5cffad193200000d00eac8bd",
                "method": "POST",
                "body": {
                    "video": "$body.video"
                }
            },
            {
                "key": "account",
                "host": "http://www.mocky.io",
                "path": "/v2/5cffad193200000d00eac8bd",
                "method": "POST",
                "body": "$body.account"
            }
        ],
        "response": { ... }
    }]
}

This should do the trick.

With Cluer you can easily change your response according to what your consumers expect. It's defined by adding a "response" property to your backend. By default, you always need to have a "root" property. The root will tell Cluer how to compose your response, and you can define it by using an array or a custom object.

When pointing the root to an array of backend responses, you should specify exactly which keys you want to add to the root of your response, like this:

{
    endpoints: [{
        "path": "/api/aggregate",
        "backends": [
            { "key": "account", ... }, 
            { "key": "videos", ... }
        ],
        "response": {
            "root": [ "$account", "$videos" ]
        }
    }]
}

However, you might want to change one specifc property of your root, or create a brand new object based on one of your backend's responses. Achieving that is very simple:

{
    endpoints: [{
        "path": "/api/aggregate",
        "backends": [
            { "key": "account", ... }, 
            { "key": "videos", ... }
        ],
        "response": {
            "root": [ "$account", "$videos" ],
            "set": {
                "user_video_url": "$videos.url",
                "accountId": "$videos.url"
            }
        }
    }]
}

Be aware that if in your "set" object you create a property which already exists in your root, it will be replaced to the new value.

Now let's suppose you want to compose your root from scratch, you can do it by assigning root to a new object:

{
    endpoints: [{
        "path": "/api/aggregate",
        "backends": [
            { "key": "account", ... }, 
            { "key": "videos", ... }
        ],
        "response": {
            "root": {
                "account_name": "$account.name",
                "account_id": "$account.id",
                "videos": {
                    "url": "$videos.url",
                    "description": "$videos.name"
                }
            }
        }
    }]
}

You can use lifecycle methods to intercept and change the behaviour of your API. For each lifecycle method you will receive a context. If you need to modify this context, you need to return a brand new object. In other words, Cluer expects your lifecycle methods to be pure functions.

Using lifecycle methods is as simple as putting a require() in your code. You just need to point your endpoint to the file where your methods are:

{
    "endpoints": [{
        "path": "/api/aggregate",
        "lifecycleHandlers": "./myHandlers.js",
        "backends": [ ... ],
        "response": { ... }
    }]
}

Now that you told Cluer to use a lifecycle handler, all you need to do is to create a file exporting the methods you want to call in the middle of your execution. Note that you can see a full usage of this inside the examples folder.

async function beforeAnyBackendRequest(context) {
	console.log('1 - fired before any backend request', context);
	return context;
}

async function afterAllBackendRequests(context) {
	console.log('2 - fired after all backends returned', context);
	return context;
}

async function onAnyBackendRequestError(error) {
	console.log('3 - fired on any backend error', error);
	return error;
}

async function onFinishMappingResponse(context) {
    console.log('4 - fired after our custom response was mounted', context);
	return context;
}

const account = {
	async beforeBackendRequest(context) {
		console.log('5 - fired before ACCOUNT backend request', context);
		return context;
	},
	async afterBackendRequest(context) {
		console.log('6 - fired after ACCOUNT backend returned', context);
		return context;
	},
	async onBackendRequestError(error) {
		console.log('7 - fired if ACCOUNT backend request throws an error', error);
		return error;
	}
};

const videos = {
	async beforeBackendRequest(context) {
		console.log('5 - fired before VIDEOS backend request', context);
		return context;
	},
	async afterBackendRequest(context) {
		console.log('6 - fired after VIDEOS backend returned', context);
		return context;
	},
	async onBackendRequestError(error) {
		console.log('7 - fired if VIDEOS backend request throws an error', error);
		return error;
	}
};

module.exports = {
	beforeAnyBackendRequest,
	afterAllBackendRequests,
	onAnyBackendRequestError,
	onFinishMappingResponse,
	account,
	videos
};

Help improving Cluer. Send a PR and feel free to change it as you need. For any questions you have, send us a message! Cheers.