Awesome Open Source
Awesome Open Source

Alagarr is a request-response helper library for serverless/faas functions* invoked via HTTP events (e.g. via API Gateway). Alagarr makes your code portable: it abstracts the event-context-callback function signatures of various serverless-providers so that you can spend less time writing boring function-as-a-service-related boilerplate.

Alagarr is a higher-order function which abstracts the the programming models of various serverless-cloud providers and adds a standardized request-response model extensible through composable middleware functions. It's API is concise and will be familiar to anyone who's worked with Express.js. It comes with built-in error handling which makes it trivial to implement error-recovery strategies.

*Currently: AWS Lambda/API Gateway. Next: GCP & Azure

CircleCI Coveralls David David

Without Alagarr:

// AWS Lambda / API Gateway
module.exports.myHandler = function(event, context, callback) {
  callback(null, {
    statusCode: 200,
    body: JSON.stringify({ foo: 'bar' }),
    headers: {
      'content-type': 'application/json',
    },
  })
}

With Alagarr:

const alagarr = require('alagarr')

module.exports.myHandler = alagarr(() => ({ foo: 'bar' }))

Contents

  1. Features
  2. Full Example
  3. Installation & Usage
  4. Configuration
    1. Options
  5. API
  6. Error Handling
  7. Logging
  8. Middleware
    1. Request Middleware
    2. Response Middleware
    3. Custom Middleware
  9. Contributing
  10. Similar Projects
  11. Related Thingies
  12. License

Features

  • Concise & familiar API
  • Zero dependencies
  • Fully tested
  • Built-in error handling makes catching and throwing errors a breeze
  • Kibana-ready request logging
  • Middleware for common tasks included
  • Request cookie parsing
  • Normalized request headers
  • Includes request body parsers
  • Response CSP headers
  • Response gzipping/deflate
  • Easily respond with images/binary data
  • Support for custom middleware

Full Example

Alagarr helps you cut out all the boilerplate involved with handling HTTP requests in serverless functions. Albeit somewhat contrived, here is a before-and-after example of a common pattern frequently found in AWS Lambda function's:

Without Alagarr 馃槶

const got = require('got')

module.exports.handler = function(event, context, callback) {
  const { queryStringParameters: { currency } } = event

  if (!currency) {
    callback(null, {
      statusCode: 400,
      body: JSON.stringify({
        message: 'Please provide the "currency" query parameter.',
      }),
      headers: {
        'content-type': 'application/json',
      },
    })
  }

  got(`https://api.coinmarketcap.com/v1/ticker/${currency}`)
    .then(response => {
      callback(null, {
        statusCode: 200,
        body: JSON.stringify(response),
        headers: {
          'content-type': 'application/json',
        },
      })
    })
    .catch(error => {
      callback(null, {
        statusCode: error.statusCode,
        body: JSON.stringify({
          error: error.response,
        }),
        headers: {
          'content-type': 'application/json',
        },
      })
    })
}

With Alagarr 馃榿

const { alagarr, ClientError } = require('alagarr') // @TODO: this require is wrong
const got = require('got')

module.exports.handler = alagarr((request, response) => {
  const { query: { currency } } = request

  if (!currency) {
    throw new ClientError('Please provide the "currency" query parameter.')
  }

  return got(`https://api.coinmarketcap.com/v1/ticker/${currency}`)
})

There are a few things being handled for you in the above Alagarr example:

  • The programming model has been normalized. You can run this code without modification on any of the supported cloud/faas/serverless providers. Not just AWS Lambda. Alagarr makes your code portable.
  • The callback() is being handled for you. Alagarr will set the status code, content-type, and body appropriately. More on this behavior here.
  • Errors are caught for you and turned into something appropriate for the client based on the type of error. If you don't like the default behavior, you can provide your own error handler.

Installation & Usage

Install Alagarr with NPM or Yarn:

npm install alagarr

Then include it in your serverless function:

const alagarr = require('alagarr')

module.exports.exampleHandler = alagarr(request => {
  const { path, provider } = request

  return `You've ended up at ${path} on the ${provider} cloud.`
})

Configuration

Alagarr ships with default configuration that should work for most use-cases. But, it's possible to pass a configuration object as the second parameter to the alagar() function:

const alagarr = require('alagarr')

module.exports.handler = alagarr(() => 'Hello world!', {
  headers: {},
  logger: console.log,
})

Configuration Options

The available configuration options are outlined here:

Option Default Description
cspPolicies [] List of CSP policies to include in the response headers
errorHandler Provide a custom error handler. See the section on Error Handling for more details
headers {} Headers to include in every response
logger Logger to use to log requests. If undefined, Alagarr will use an internal logger. Logging can be disabled by setting to false. See the section on Logging for more details
requestMiddleware Array of custom request middleware to use. See the section on Request Middleware for more details
responseMiddleware Array of custom response middleware to use. See the section on Response Middleware for more details

API

Alagarr module

Request methods

Response methods


alagarr(handlerFunction, configurationOptions?): void

@TODO

const alagarr = require('alagarr')

const configurationOptions = {
  logger: false,
}

const handlerFunction = function(request, response) {
  const { query: { name } } = request
  return response.html(`Hello ${name}.`)
}

module.exports.handler = alagar(handlerFunction, configurationOptions)

The handlerFunction has a function signature of:

export type HandlerFunction = (
  request: any,
  response: any,
) => string | object | void | Promise<string | object | void>

If your handlerFunction returns falsey, then it's your responsibility to call the appropriate response method to end the invocation (e.g. response.json()). For convenience, if the handlerFunction returns a string, the result will be passed to response.html() or response.text() for you. Alternatively, if the handler returns an object, it will be passed to response.json(). You may also return a Promise (or make your handler async).


requestMiddleware

@TODO


responseMiddleware

@TODO


ClientError(message, statusCode = 400)

@TODO


ServerError(message, statusCode = 500)

@TODO


request.body

The request body, if any. If using default request middleware, or another body parser, this value will contain the parsed contents of the request body.

readonly body: string | object

request.context

The provider context object.

On AWS Lambda this is the second parameter passed to a Lambda function's handler.

readonly context: object

request.cookies

An object containing the cookies included with the request.

readonly cookies: {
  readonly [name: string]: string
}

request.headers

An object containing all of the headers included in the request.

readonly headers: {
  readonly [name: string]: string
}

request.hostname

The request's hostname. Derived from the request's Host header.

readonly hostname: string

request.meta

An object containing some meta data about the invocation. It includes:

readonly meta: {
  readonly coldStart: boolean, // was this a cold start?
  readonly invocationCount: number, // number of times this container has been invoked
}

request.method

The request HTTP method. E.g GET or POST.

readonly method: enum {
  'GET',
  'POST',
  'PATCH',
  'DELETE',
}

request.path

The request path.

readonly path: string

request.provider

The name of the current request's provider. Possible values include: aws

readonly provider: enum {
  'aws'
}

request.query

An object of query parameters included in the request.

Given a request:

GET http://example.com?foo=1&bar=2

request.query will contain:

{
  foo: '1',
  bar: '2',
}
readonly query: {
  readonly [name: string]: string
}

request.source

The name of the current request's invocation source. Possible values include: api-gateway

readonly source: enum {
  'api-gateway'
}

request.timestamp

Timestamp at the time of the first middleware's execution.

readonly timestamp: number

response.respondTo(formats, statusCode = 200, options = {}): void

Respond according to request's Accept header with formats provided in formats map. Kind of like a Ruby on Rails respond_to do |format| block.

response.respondTo({
  json: {},
  html: '<html />',
})

response.raw(error: Error | null, result?: object | boolean | number | string): void

Exposes the underlying callback method.

response.raw(null, { something: 'custom' })

Error Handling

Throw em. Alagarr will catch them.

@TODO

Logging

Yes.

@TODO

Middleware

Alagarr uses a pipeline of middleware functions to process the incoming request and outgoing response objects. This lets you customize how your requests and responses are handled as well as provide custom middleware in addition to those provided by Alagarr.

Request Middleware

Alagarr includes the following request middleware:

Provider Name Default Description
All meta built-in Adds meta data about the request including whether the invocation is a coldStart, and invocation count
All normalize-headers built-in Normalizes request headers.
All normalize-programming-model built-in Normalizes the programming models of different providers.
All timestamp built-in Adds a request-start timestamp under request.timestamp which can be used to determine the ellapsed duration of the invocation
Any cookies enabled Parses cookies out of request header and makes them accessible under request.cookies
Any hostname enabled Sets a convenience hostname property on the request object based on the request headers
Any json-body enabled Body parser for request bodies with content-type of application/json
Any url-encoded-body enabled Body parser for request bodies with content-type of application/x-www-form-urlencoded
AWS base64-body enabled Decodes base64-encoded request bodies when isBase64Encoded on the API Gateway request is truthy

Response Middleware

Alagarr includes the following response middleware:

Provider Name Default Description
All enforced-headers built-in
All log built-in
Any compress disabled Compress response body with deflate or gzip when appropriate
Any content-length enabled Adds a content-length header to the response
Any csp enabled Adds Content-Security-Policy headers to the response
Any etag disabled Adds an Entity Tag (ETag) header to the response

Custom Middleware

All middleware are functions. Middleware which is included in Alagarr are all pure, but this is not required for custom middleware. Middleware may return Promises which are resolved before the next middleware is called. Middleware should not mutate state, but instead return new values鈥攂ut this is not required in custom middleware. However, everytime middleware mutates state, a cute cuddly koala dies somewhere in Australia. So.. Yea.

Request middleware act on a request object and must always return a new request object. Request middleware have the following function signature:

type requestMiddleware = (
  request: InterfaceRequest,
  options: InterfaceAlagarrOptions,
) => InterfaceRequest

Response middleware act on the response object and must always return a new response object. Response middleware have the following function signature:

type responseMiddleware = (
  response: InterfaceResponseData,
  request: InterfaceRequest,
  options: InterfaceAlagarrOptions,
) => InterfaceResponseData

Example

An example of custom middleware might be middleware which handles user sessions. The request middleware would restore a session from some data store, while the response middleware might ensure a session is updated and a cookie is set.

Request Middleware

module.exports.restoreSession = async function(request) {
  const { cookies: { sessionId } } = request
  const session = (await getSessionFromDatabase(sessionId)) || undefined

  return {
    ...request,
    session,
  }
}

Response Middleware

module.exports.saveSession = async function(responsePayload, request) {
  const sessionCookie = await saveSessionToDatabase(request.session)

  return {
    ...responsePayload,
    headers: {
      ...responsePayload.headers,
      'Set-Cookie': `session=${sessionCookie}`, // @TODO: refactor once #5 is closed.
    },
  }
}

This custom middleware could then be used with Alagarr in a serverless function handler with:

const handler = require('alagarr')
const { restoreSession, saveSession } = require('./custom-middleware')

const alagarrConfig = {
  requestMiddleware: ['default', restoreSession],
  responseMiddleware: ['default', saveSession],
}

module.exports.userDashboardHandler = handler((request, response) => {
    const session = request.session

    if (!session) {
      return response.redirect('/login')
    }

    return `<h1>Welcome back, ${session.username}!</h1>`
  }
  alagarrConfig,
)

Contributing

The codebase tries to follow declarative, functional(ish) programming paradigms. Many functional styles are enforced through TSLint linter utilised by the project. These include immutablity rules (no-let, no-object-mutation) and rules which prohibit imperative code (no-expression-statement, no-loop-statement). Disabling the linter for code should be avoided. Exceptions are made where satisfying a linting rule is impractical or otherwise untenable. In practice, this tends to be areas where the code touches 3rd party modules and in tests due to Jest's imperative-style.

Similar Projects

Related Thingies

License

AlagarrMarco L眉thy. Released under the MIT license.
Authored and maintained by Marco L眉thy with help from contributors.

github.com/adieuadieu 路 GitHub @adieuadieu 路 Twitter @adieuadieu 路 Medium @marco.luethy


Get A Weekly Email With Trending Projects For These Topics
No Spam. Unsubscribe easily at any time.
typescript (9,917)聽
serverless (611)聽
aws-lambda (300)聽
request (80)聽
faas (74)聽
no-dependencies (67)聽
response (29)聽
aws-apigateway (19)聽

Find Open Source By Browsing 7,000 Topics Across 59 Categories

Advertising 馃摝聽10
All Projects
Application Programming Interfaces 馃摝聽124
Applications 馃摝聽192
Artificial Intelligence 馃摝聽78
Blockchain 馃摝聽73
Build Tools 馃摝聽113
Cloud Computing 馃摝聽80
Code Quality 馃摝聽28
Collaboration 馃摝聽32
Command Line Interface 馃摝聽49
Community 馃摝聽83
Companies 馃摝聽60
Compilers 馃摝聽63
Computer Science 馃摝聽80
Configuration Management 馃摝聽42
Content Management 馃摝聽175
Control Flow 馃摝聽213
Data Formats 馃摝聽78
Data Processing 馃摝聽276
Data Storage 馃摝聽135
Economics 馃摝聽64
Frameworks 馃摝聽215
Games 馃摝聽129
Graphics 馃摝聽110
Hardware 馃摝聽152
Integrated Development Environments 馃摝聽49
Learning Resources 馃摝聽166
Legal 馃摝聽29
Libraries 馃摝聽129
Lists Of Projects 馃摝聽22
Machine Learning 馃摝聽347
Mapping 馃摝聽64
Marketing 馃摝聽15
Mathematics 馃摝聽55
Media 馃摝聽239
Messaging 馃摝聽98
Networking 馃摝聽315
Operating Systems 馃摝聽89
Operations 馃摝聽121
Package Managers 馃摝聽55
Programming Languages 馃摝聽245
Runtime Environments 馃摝聽100
Science 馃摝聽42
Security 馃摝聽396
Social Media 馃摝聽27
Software Architecture 馃摝聽72
Software Development 馃摝聽72
Software Performance 馃摝聽58
Software Quality 馃摝聽133
Text Editors 馃摝聽49
Text Processing 馃摝聽136
User Interface 馃摝聽330
User Interface Components 馃摝聽514
Version Control 馃摝聽30
Virtualization 馃摝聽71
Web Browsers 馃摝聽42
Web Servers 馃摝聽26
Web User Interface 馃摝聽210