ServerlessJS takes one hundred thousand years to deploy small changes, so it's faster to test locally before shipping to dev.
Also, 3rd party libraries are the devil, so it's often better to roll your own.
The code below is copy-paste-able into your Serverless project to keep your build small, and to keep your project safe.
This file itself requires some 3rd party devil libraries
Not everything is tested or implemented
Add your routes to the "router" object near the top of the file.
{
'/the/api/path': theHandlerFunction,
'/{parameters}/also/work': aFunctionThatTakesParameters,
}
By default this runs on port 8888, but that can be changed by updating the value in listen(8888) near the bottom.
This setups a server that
http://localhost:8888router maphttp://localhost:8888/user/1234 -> http://localhost:8888/user/{userId} -> {userId: 1234}handler(req: APIGatewayEvent))import http from "http";
import { info, error } from "console";
import AiInsightsCms from './lambdas/reports/ai-insights-cms.local';
import { APIGatewayEvent } from "aws-lambda";
// Add your routes here, note that we're blissfully
// ignoring HTTP method
const router = {
'/ai-insights/{websiteId}': AiInsightsCms
}
// Awaitable helper to parse the request
// body stream into a string
function parseBody(r: http.IncomingMessage) {
return new Promise((resolve) => {
const chunks = [] as Buffer[];
r.on('data', d => {
chunks.push(d);
}).on('end', () => {
resolve(Buffer.concat(chunks).toString())
});
});
}
// Create the HTTP listener
http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {
info(`Received request: ${req.method} ${req.url}`);
try {
// Parse the request
const uri = new URL(req.url!, `http://${req.headers.host}`);
// Initialize variables to hold the handler and params
// in case we find a matching route
let handler: Function | null = null;
let params: { [key: string]: string } = {};
// Iterate each configured route and extract the params
// and handler if one is found that matches the request
for (const [route, handlerFn] of Object.entries(router)) {
// Sub out the route parameters with regex
const regex = new RegExp('^' + route.replace(/{[^}]+}/g, '([^/]+)') + '$');
const match = uri.pathname.match(regex);
if (match) {
// Extract the parameter names stripping off the {} with slice(1,-1)
const paramNames = (route.match(/{[^}]+}/g) || []).map(name => {
return name.slice(1, -1);
});
// Map the parameter names to their values
paramNames.forEach((name, index) => {
params[name] = match[index + 1];
});
// Save the handler function and stop iterating
handler = handlerFn;
break;
}
};
// Return 404 if no handler found
if (!handler) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not Found' }));
return;
}
// Build a mock API Gateway event
// with the path parameters and body
// parsed from the request
const apiGatewayReq = {
httpMethod: req.method!,
headers: req.headers,
path: req.url!,
pathParameters: params,
body: await parseBody(req)
} as APIGatewayEvent;
// Call the handler function
const handlerResp = await handler(apiGatewayReq);
info(`Success - handler response:`, handlerResp);
res.writeHead(200);
res.write(JSON.stringify(handlerResp, null, 4));
res.end();
} catch (e) {
error('Error handling request', e);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Internal Server Error' }));
}
}).listen(8888);
info("Static file server running at\n => http://localhost:8888/\nCTRL + C to shutdown");
yarn ts-node local-server.ts
# But I have envvars
VARIABLE1=test yarn ts-node local-server.ts
Or if you have VS Code, just copy this launch config and press F5. Envvars are handles using the dotenv package.
{
"version": "0.2.0",
"configurations": [
{
"type": "node-terminal",
"request": "launch",
"name": "Run local API Gateway server",
"command": "yarn ts-node local-server.ts",
"envFile": "${workspaceFolder}/.env"
}
]
}