Swambda
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.
Note! Project is not complete and not ready for production!
You can make a difference, too, by sending pull requests.
Concept
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;
}
Basic setup
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) => {
const swagger = require("yml-loader!./swagger.yaml");
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) => {
callback(null, result);
})
.catch((err) => {
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()
.preProcessor(preProcessor)
.postProcessor(postProcessor)
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) => {
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!
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
FAQs
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).