Invoke API Gateway Endpoints Locally
Our notes app backend has an API Gateway endpoint. We want to be able to develop against this endpoint locally. To do this we’ll use the serverless-offline plugin to start a local web server.
Invoke API locally
We installed the above plugin at the repo root, because all API services require the plugin. Open serverless.yml
in our notes-api
. You’ll notice serverless-offline
is listed under plugins.
service: notes-api
plugins:
- serverless-offline
...
Let’s start our local web server.
$ cd notes-api
$ serverless offline
By default, the server starts on http://localhost
and on port 3000
. Let’s try making a request to the endpoint:
$ curl http://localhost:3000/notes
Mocking Cognito Identity Pool authentication
Our API endpoint is secured using Cognito Identity Pool. The serverless-offline plugin allows you to pass in Cognito authentication information through the request headers. This allows you to invoke the Lambdas as if they were authenticated by Cognito Identity pool.
To mock a User Pool user id:
$ curl --header "cognito-identity-id: 13179724-6380-41c4-8936-64bca3f3a25b" \
http://localhost:3000/notes
You can access the id via event.requestContext.identity.cognitoIdentityId
in your Lambda function.
To mock the Identity Pool user id:
$ curl --header "cognito-authentication-provider: cognito-idp.us-east-1.amazonaws.com/us-east-1_Jw6lUuyG2,cognito-idp.us-east-1.amazonaws.com/us-east-1_Jw6lUuyG2:CognitoSignIn:5f24dbc9-d3ab-4bce-8d5f-eafaeced67ff" \
http://localhost:3000/notes
And you can access this id via event.requestContext.identity.cognitoAuthenticationProvider
in your Lambda function.
Working with multiple services
Our app is made up of multiple API services; notes-api
and billing-api
. They are two separate Serverless Framework services. They respond to /notes
and /billing
path respectively.
The serverless-offline plugin cannot emulate an overall API endpoint. It cannot handle requests and route them to the corresponding service that is responsible for it. This is because the plugin works on the service level and not at the app level.
That said, here is a quick script that lets you run a server on port 8080
while routing /notes
and /billing
to their separate services.
#!/usr/bin/env node
const { spawn } = require('child_process');
const http = require('http');
const httpProxy = require('http-proxy');
const services = [
{route:'/billing/*', path:'services/billing-api', port:3001},
{route:'/notes/*', path:'services/notes-api', port:3002},
];
// Start `serverless offline` for each service
services.forEach(service => {
const child = spawn('serverless', ['offline', 'start', '--stage', 'dev', '--port', service.port], {cwd: service.path});
child.stdout.setEncoding('utf8');
child.stdout.on('data', chunk => console.log(chunk));
child.stderr.on('data', chunk => console.log(chunk));
child.on('close', code => console.log(`child exited with code ${code}`));
});
// Start a proxy server on port 8080 forwarding based on url path
const proxy = httpProxy.createProxyServer({});
const server = http.createServer(function(req, res) {
const service = services.find(per => urlMatchRoute(req.url, per.route));
// Case 1: matching service FOUND => forward request to the service
if (service) {
proxy.web(req, res, {target:`http://localhost:${service.port}`});
}
// Case 2: matching service NOT found => display available routes
else {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write(`Url path "${req.url}" does not match routes defined in services\n\n`);
res.write(`Available routes are:\n`);
services.map(service => res.write(`- ${service.route}\n`));
res.end();
}
});
server.listen(8080);
// Check match route
// - ie. url is '/notes/123'
// - ie. route is '/notes/*'
function urlMatchRoute(url, route) {
const urlParts = url.split('/');
const routeParts = route.split('/');
for (let i = 0, l = routeParts.length; i < l; i++) {
const urlPart = urlParts[i];
const routePart = routeParts[i];
// Case 1: If either part is undefined => not match
if (urlPart === undefined || routePart === undefined) { return false; }
// Case 2: If route part is match all => match
if (routePart === '*') { return true; }
// Case 3: Exact match => keep checking
if (urlPart === routePart) { continue; }
// Case 4: route part is variable => keep checking
if (routePart.startsWith('{')) { continue; }
}
return true;
}
This script is in included as startServer
in the sample repo. But let’s quickly look at how it works. It has 4 sections:
- At the very top, we define the services we are going to start. Tweak this to include any new services that you add.
- We then start each service based on the port defined using the serverless-offline plugin.
- We start an HTTP server on port 8080. In the request handling logic, we look for a service with a matching route. If one is found, the server proxies the request to the service.
- At the bottom, we have a function that checks if a route matches a url.
You can run this server locally from the project root using:
$ ./startServer
Now that we have a good idea of how to develop our Lambda functions locally, Let’s look at what happens when you want to create an environment for a new feature.
For help and discussion
Comments on this chapterIf you liked this post, please subscribe to our newsletter, give us a star on GitHub, and follow us on Twitter.