1.2.0 ā€¢ Published 4 years ago

azure-monofunction v1.2.0

Weekly downloads
3
License
MIT
Repository
github
Last release
4 years ago

Azure Monofunction is a router for Single-Function APIs (SFA) that makes possible for you to develop monolithic APIs and still use the power of serverless, like cost-per-consumption and automatic scalability.

See all features.

Table of contents


Installation

Requirements

npm.io npm.io npm.io npm.io

Azure Monofunction was tested for the environments below. Even we believe it may works in older versions or other platforms, it is not intended to.

EnvironmentTested version
OSUbuntu 20.04
Node.js12.16.3
Package Managernpm 6.14.5
PlatformsAzure Functions Host v2 and v3

Installing

Via package manager

npm.io

$ npm install --save azure-monofunction

Configuring the single function

  1. Create a HTTP trigger in you function app. It can have any name, like "monofunction".

  2. Change the route property for the trigger binding in function.json to a wildcard, like {*segments}. You can copy the function.json template.

  3. Create the function index.js at the function folder and start develop following the basic usage below.


Usage

npm.io npm.io

TL;DR

The most simple usage

const app = require('azure-monofunction');

const routes = [{
  path: '/example/:param',
  methods: ['GET', 'PUT'],
  run: async (context) => {
    context.res.body = { it: 'works' };
  },
}]

app.addRoutes(routes);

module.exports = app.listen();

The anatomy of a simple route is an URL path, its HTTP methods to match and some middleware to run.

An alternative to the addRoutes method that supports a list of route objects is the method route that adds a single route to the router.

app.route(['GET','POST'], '/example/path', async (context) => {
  // ...middleware logic
});

Defining route paths

Route paths must always start with / and be followed by valid URL paths.

You can define dynamic parameters prefixing a path level with :.

For example, the URL /users/123 will match the route /users/:id.

The parameter will be available at the context.params object.

Example:

app.route(['GET'], '/users/:id', async (context) => {
  const userId = context.params.id;
});

Defining the HTTP methods for a route

HTTP verbs must always be defined as arrays of strings both in route objects and route method.

The supported HTTP verbs are: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'].

Additionally, you can add routes with verbs corresponding methods: get(), post(), patch(), put(), delete().

app.addRoutes([{ 
  methods: ['GET', 'POST'],
  path,
  run,
}]);

// is the same of:

app.route(['GET', 'POST'], path, run);

// and the same of:

app.get(path, run); // feels like Express šŸ™‚

Using middlewares for routes

Middlewares works with the power of Azure Functions Middlewares and are asynchronous functions that takes a required context argument and an optional STOP_SIGNAL argument.

Request object is available through context.req property and response should be set using the context.res property. Other Azure Functions context properties are also available.

When defining a middleware for a route with the run property/argument, you can use a single middleware function or an array of middlewares.

Example:

const headerMiddleware = async (context) => {
  context.res.headers['X-User-ID'] = context.params.id;
  context.anything = true;
};

const resourceMiddleware = async (context) => {
  context.res.body = { added: context.anything };
};

app.get('/posts/:id', [headerMiddleware, resourceMiddleware]);

// output of /posts request should be

// HTTP 200 /posts/123
// X-User-ID: 123
// { "added": true }

It is strongly recommended you read Azure Functions Middlewares docs, but, if you can't, please note:

  • Always use asynchronous functions as middlewares.
  • Do not return anything inside your middleware function, unless you want to throw an error.
  • You can pass values to the next middlewares using the context object reference.
  • Return the STOP_SIGNAL argument in the middleware if you need to prevent any following middleware to be executed. This is useful for Content-type negotiation or Authorization.
  • See other context-related docs in Accessing and modifying the context at the Azure Functions Middlewares reference.
  • Common community middlewares are available under the Azure Functions Middlewares project.

Controller architecture

Using a single-function API leads you back to the need of middleware reusability and composition.

This monolithic approach is more maintainable than microservices approach, but requires more organization.

When dealing with many routes and its middlewares, you certainly will fall to a controller separation pattern.

Controllers are often separated by resource entities or related services.

It is also good to separate your routes inside a route file for better reading.

Example:

Function directory tree

.
ā”œā”€ā”€ controllers
ā”‚   ā””ā”€ā”€ user.controller.js
ā”œā”€ā”€ monofunction
ā”‚   ā”œā”€ā”€ function.json
ā”‚   ā”œā”€ā”€ index.js
ā”‚   ā””ā”€ā”€ routes.js
ā””ā”€ā”€ host.json

/controllers/user.controller.js

async function getUser({ req, res, params }) {
  context.res.body = await db.getUser(params.id);
}

async function createUser({ req }) {
  context.res.status = await db.createUser(req.body);
}

module.exports = {
  getUser,
  createUser,
};

/monofunction/routes.js

const userController = require('../controllers/user.controller');

module.exports = [{
  path: '/users/:id',
  methods: ['GET'],
  run: userController.getUser,
}, {
  path: '/users',
  methods: ['POST'],
  run: userController.createUser,
}];

/monofunction/index.js

const app = require('azure-monofunction');
const routes = require('./routes');

app.addRoutes(routes);

module.exports = app.listen();

Capturing errors

If a middleware returns a value or throws an error, an error will also be forwarded to Azure Functions Host execution.

If you want to catch this unhandled errors, you can use the onError handler.

app.onError((error, context) => {
  // error argument contains the original value
  // context is preserved until here
});

Global middlewares

Sometimes you need to run common middlewares for all the routes, regardless its resources, just like headers validation, authorization rules and content-type negotiation and parsing.

You can add middlewares for all the routes ("global middlewares") using the use() method.

app.use(async (context) => {
  context.res.headers['X-Powered-By'] = 'ā›ˆ Azure Monofunction';
});

app.addRoutes(routes);

// now all the routes will respond with a 'X-Powered-By' header

You can also add a conditional global middleware calling the useIf() method from Azure Functions Middlewares.

const isChromeAgent = (context) => {
  return context.req.headers['User-Agent'].indexOf('Chrome') !== -1;
}

app.useIf(isChromeAgent, async (context) => {
  context.res.headers['Content-Type'] = 'text/html';
});

// now if a route was called from Chrome browser, the response will be set to HTML COntent-Type

Find useful built middlewares in Common community middlewares of Azure Functions Middlewares.

Defining a not found fallback

If no route was matched during a request, Azure Monofunction will throw a not found error.

But if you want to handle this not found event with a fallback route, you can use the on404() handler.

app.on404((context) => {
  // add your fallback logic here, like:
  context.res.status = 404;
});

Route custom metadata

You can add custom route metadata in route object's meta property that will be available in context.meta property:

const routes = [{
  path: '/route',
  methods,
  meta: {
    something: 'value',
  },
  run: async (context) => {
    context.res.body = context.meta;
    // body will be { "something": "value" }
  },
}];

This is useful when you need to recover this meta in other middlewares, specially conditional middlewares, like an authorization middleware:

const hasAuth = (context) => context.meta && context.meta.auth;
app.useIf(hasAuth, async (context, STOP_SIGNAL) => {
  if (!context.req.headers.Authorization) {
    context.res.status = 401;
    return STOP_SIGNAL;
  }
});

app.addRoutes([{
  path: '/resource',
  methods: ['POST'],
  meta: {
    auth: true
  },
  run,
}, {
  path: '/resource',
  methods: ['GET'],
  run,
}]);

// POST /resource without an Authorization header will return HTTP 401
// but GET /resource will not

Debugging and customizing logger

You can log everything that is done by Azure Monofunction setting the debug property to true.

app.debug = true;

You can also use a different logger than console/context setting it in the logger property.

app.logger = MyCustomLogger;

ā„¹ Note that your logger need to have a log(message, ...args) method.

Route prefix

Azure Functions Host has a route prefix for all the requests. This defaults to /api but you can customize it in host.json:

{
  "extensions": {
    "http": {
      "routePrefix": "api",
    }
  }
}

If you customize the route prefix, Azure Monofunction will try to guess it from extensions.http.routePrefix configuration defined in host.json file.

If in a parallel universe you did not defined the route prefix in host.json but your function app has a route prefix different than /api, you need to specify that in the Azure Monofunction's routePrefix property.

app.routePrefix = '/notapi';

Make sure if you need to change it you did this changing before adding any route.


Extending

Azure Monofunction is not intended to be extensible, but the middleware approach is extensible itself.

If you want to publish a middleware (or an evaluation function) you developed and think it will be useful for any other developer, see Writing and publishing common middlewares in Azure Functions Middlwares.


Help

Support

npm.io

If you need help or have a problem with this project and you not found you problem in FAQ above, start an issue.

We will not provide a SLA to your issue, so, don't expect it to be answered in a short time.


API

AzureMonofunctionRoute class

Fields

The path should be matched for the route.

The HTTP verbs that should be matched for the route.

A single middleware function or an array of middleware functions.

A middleware should be in form of async function (context, STOP_SIGNAL?):any, as documented in asyncMiddleware specification.

Custom route metadata to be available at context.meta property.

AzureMonoFunction class

Fields

Determines if Azure Monofunctions operations should be logged or not.

The logger object containing a log(message, ...args) function that will be used for logging messages.

The route prefix that will be used in route matching.

Methods

Adds a list of routes to the monofunction router.

Arguments

ArgumentTypeRequiredDefaultDescription
routesAzureMonofunctionRoute[]trueA list of valid routes.

Adds a single route to the monofunction router for HTTP verb corresponding to the method name.

It is an alias for the method route(), but with the argument methods already defined.

Returns the Azure Functions entrypoint async (context) => {} that will be called by the function HTTP trigger and will execute the entire router.

Returns

AzureFunction: the Azure Functions entrypoint.

Registers an error callback to be called when a middleware throws an error.

Arguments

ArgumentTypeRequiredDefaultDescription
catchCallbackfunctiontrueA callback function that takes two arguments (error, context)

Callbacks

catchCallback

function (error, context):any

ArgumentTypeRequiredDefaultDescription
errorError|anytrueThe error thrown by a middleware
contextContext ā„¹trueThe Azure Function context object.

Returns: anything returned by the callback will be ignored.

Registers a fallback handler to be called when no route matches the current URL.

Arguments

ArgumentTypeRequiredDefaultDescription
notFoundHandlerfunctiontrueA callback function that takes one argument (context)

Callbacks

notFoundHandler

function (context):void

ArgumentTypeRequiredDefaultDescription
contextContext ā„¹trueThe Azure Function context object.

Returns: anything returned by the callback will be ignored.

Adds a single route to the monofunction router.

Arguments

ArgumentTypeRequiredDefaultDescription
methodsArray<'GET'|'POST'|'PATCH'|'PUT'|'DELETE'>trueList of HTTP verbs for the route.
pathstringtrueThe URL path for the route.
middlewaresAsyncGeneratorFunction|AsyncGeneratorFunction[]truedefaultA single function middleware or a list of middlewares that should run for the route, in form of async function (context, STOP_SIGNAL?):any, as documented in asyncMiddleware specification.

Adds a global middleware to be executed for every matched route.

Identical to use() method from Azure Functions Middlewares.

Adds a global middleware to be conditionally executed for every matched route if the specified expression returns true.

Identical to useIf() method from Azure Functions Middlewares.


Tecnhical concepts

Motivation and design

Azure Monofunction borned to help developers use the power of serverless in Azure Functions but with minimal complexity with function management.

When using serverles, developers often end up with a microservices architecture, even with functions in the same Function App. Each function requires a HTTP binding, your own endpoint and an endpoint maps only to a single resource.

This complexity leads developers to create many triggers and functions to simple resource APIs, like CRUD APIs: you will ever need at least two functions (one for /resource and one for /resource/:id) and at least five if clauses in these functions to make it possible.

Then, in the end you always end up with two options: mix logic and confuse next API maintainers or deal with a lot of functions for basic operations that could be aggregated.

You probably experienced this, as we experienced at NOALVO, and you sure were not satisfied with any of these two approaches.

If you want to keep up with Azure Functions powers, like cost-per-consumption and elastic, automatic scalability, now you can build a monolithic API architecture, just like in Express, Koa or HAPI, using Azure Monofunction.

Azure Monofunction was inspired specially from Express monolithic APIs.

Curiosity: Azure is the cloud ā˜, Functions is the flash āš”, Azure Functions Middlewares is the thunder šŸŒ© and Azure Monofunction is the thunderstorm ā›ˆ.

Features

  • Multilevel route matching
  • Dynamic route params
  • Route HTTP verbs
  • Route middleware
  • Route metadada
  • Multiple middlewares (cascade) per route
  • Global standard and conditional middlewares
  • Error handler middleware
  • Not found/404 handler middleware

Related projects


Contributing

If you don't want to code

Help us spreading the word or consider making a donation.

Star the project

npm.io

Tweet it

npm.io

Add your company name to the Who is using secion

Make a pull request or start an issue to add your company's name.

If you want to code

npm.io

Code of conduct

npm.io

We follow Contributor Covenant Code of Conduct. If you want to contribute to this project, you must accept and follow it.

SemVer

npm.io

This project adheres to Semantic Versioning 2.0.0.

Roadmap

If you are not solving an issue or fixing a bug, you can help developing the roadmap below.

  • Improve documentation
  • Conditional middlewares for routes
  • Async error and not found handlers

Hall of fame

Who is using


License

npm.io

Licensed under the MIT License.


1.2.0

4 years ago

1.1.0

4 years ago

1.0.0

4 years ago

0.1.7

4 years ago

0.1.6

4 years ago

0.1.4

4 years ago

0.1.5

4 years ago

0.1.2

4 years ago

0.1.3

4 years ago

0.1.1

4 years ago

0.1.0

4 years ago