Integrate swagger specifications and lambda into an easy-to-use routing tier. This package is optimized for use with the netlify system.
By utilizing an OpenAPI specification and the swambda project, developers can easily implement full REST API applications in Lambda without using an API gateway or other infrastructure. When used with the Netlify system, you can host efficient, scalable REST APIs for free, while using the entire ecosystem of the OpenAPI specification including spec editors, code generators, testing systems, etc.
You can make a difference, too, by sending pull requests.
Swambda will take a valid OpenAPI 2.0 specification and use a single operation
extension (x-swagger-router-controller
) and operationId
to extract arguments
and route requests directly to a controller function. The arguments will be
parsed, extracted, and coerced into the format as designated by the OAI spec.
The developer's job is then to simply implement controller functions to perform
business logic.
It is expected that each controller function will return a promise, and can optionally use a simple function to correctly format a response as appropriate for the netlify platform.
for example:
GET: /pets/{petId}
in an OAI definition:
/pets/{petId}:
get:
summary: Info for a specific pet
operationId: getPetById
x-swagger-router-controller: Pets
tags:
- pets
parameters:
- name: petId
in: path
required: true
description: The id of the pet to retrieve
type: string
responses:
200:
description: The pet requested
schema:
$ref: "#/definitions/Pet"
will call the Pets.js
javascript file method getPetById
:
const getPetById = exports.getPetById = (args) => {
const petId = args.petId;
/* return a promise and resolve with the pet data */
}
Import the project and expose a single handler function:
"use strict";
let swambda = require("swambda");
const handler = exports.handler = (event, context, callback) => {
Set a cache. For a lambda, using global
is a common practice. If outside
of a lambda-like runtime, use something else that supports a map. This also
exposes the Swambda
class globally in our example, so new Swambda()
resolves.
swambda.cacheWith(global);
const router = swambda.fromCache()
.catch((err) => {
// not in cache, need to create
// load spec as object via webpack yml-loader
const swagger = require("yml-loader!./swagger.yaml");
// set the route path
return new Swambda(event.path)
.load(swagger)
.then(router => {
return router;
});
});
Note! the event.path
variable is used during initialization to help determine
where the library responses are being served from. This eases the setup.
We now are guaranteed to have a router
object, which can process the request:
router.process(event)
.then((result) => {
// return result to the caller
callback(null, result);
})
.catch((err) => {
// catch any errors and do something meaningful
callback(null, err);
});
Note: the .catch()
is not expected to be invoked at runtime. Any reject(..)
messages should be caught inside your controller code, and passed to the
respondWith
function. That allows you to send a proper error message and
payload, not just some ugly stack trace.
You can configure optional features, so you can add authentication, response signing, etc:
new Swambda(event.path)
.cors() // enable CORS and add additional response headers
.preProcessor(preProcessor) // add a pre-processor for requests
.postProcessor(postProcessor) // add a post-processor for responses
//-> continue with loading
When configuring CORS support, you can optionally add headers in the function.
For example, if you want to allow a header called Bearer
:
new Swambda(event.path)
.cors({
"Access-Control-Allow-Headers": "Content-Type, Bearer"
})
This will merge with the "standard" CORS response headers like
Access-Control-Allow-Headers
Access-Control-Allow-Methods
Access-Control-Allow-Origin
The preProcessor
signature looks like this:
const preProcessor = function (event, args) {
return new Promise((resolve, reject) => {
// for example, detect the user from the headers using a custom function
args[user] = detectUserFromHeaders(event.headers);
resolve(args);
});
}
Note that the preProcessor can add additional arguments into the args
hash,
just be sure to avoid collisions with arguments defined in the OAI Spec.
The postProcessor
can inject additional headers, handle special error codes, etc:
const postProcessor = function (response) {
return new Promise((resolve, reject) => {
if(response.statusCode === 500) {
response.headers["x-error-code"] = "abcd123";
}
resolve (response);
})
}
Finally, implementing the business logic is easy!
// Node: `respondWith` is implicitly available with a global cache
const getPetById = exports.getPetById = (args) => {
return new Promise((resolve, reject) => {
const petId = args.petId;
if(typeof petId === "undefined") {
respondWith(resolve, 400, {
code: 400,
message: "invalid pet id"
});
return;
}
if(data[petId]) {
respondWith(resolve, 200, data[petId]);
}
respondWith(resolve, 404, {
code: 404,
message: "pet not found"
});
})
}
Here is a full example
What about performance? Use the cache during setup and it has almost zero overhead.
What parameter types are supported? Primitives and objects are currently supported. Parameters with arrays, form data are not (but easy to add).