How to debug in VS Code a local AWS Lambda function with API Gateway written in TypeScript?
Asked Answered
A

3

13

We are about to start working with Lambda functions.
We have that technology constraint that we have to use TypeScript.
I want to be able to debug my ts file in VS Code when the related endpoint is called from Postman.

So, we have the following development environment:

  • Windows 10
  • Docker for Windows (with Hyper-V not with WSL 2)
  • TypeScript 4.1
  • Node 12.17
  • SAM CLI 1.13.2

I've used sam init with the Hello World template to generate the initial folder structure.
I've enhanced it (mostly based on this article) to work with TypeScript.

Folder structure

.
├── template.yaml
├── .aws-sam
├── .vscode
|   └── launch.json
├── events
├── hello-world
|   ├── dist
|       ├── app.js
|       └── app.js.map
|   ├── src  
|       └── app.ts
|   ├── package.json
|   └── tsconfig.json

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  LambdaWithApiGateWayDebug

  Sample SAM Template for LambdaWithApiGateWayDebug

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: hello-world/dist
      Handler: app.lambdaHandler
      Runtime: nodejs12.x
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "./dist",
    "strict": true,
    "noImplicitAny": true,
    "esModuleInterop": true,
    "sourceRoot": "./src",
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

package.json

{
  "name": "hello_world",
  "version": "1.0.0",
  "description": "hello world sample for NodeJS",
  "main": "app.js",
  "repository": "https://github.com/awslabs/aws-sam-cli/tree/develop/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs",
  "scripts": {
    "compile": "tsc",
    "start": "sam local start-api -t ../template.yaml -p 5000 -d 5678"
  },
  "dependencies": {
    "@types/aws-lambda": "^8.10.64",
    "@types/node": "^14.14.10",
    "aws-sdk": "^2.805.0",
    "source-map-support": "^0.5.19",
    "typescript": "^4.1.2"
  }
}

app.ts

import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";

export const lambdaHandler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    const queries = JSON.stringify(event.queryStringParameters);
    return {
      statusCode: 200,
      body: `Queries: ${queries}`
    }
}

app.js

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.lambdaHandler = void 0;
const lambdaHandler = async (event) => {
    const queries = JSON.stringify(event.queryStringParameters);
    return {
        statusCode: 200,
        body: `Queries: ${queries}`
    };
};
exports.lambdaHandler = lambdaHandler;
//# sourceMappingURL=app.js.map

app.js.map

{"version":3,"file":"app.js","sourceRoot":"./src/","sources":["app.ts"],"names":[],"mappings":";;;AAEO,MAAM,aAAa,GAAG,KAAK,EAAE,KAA2B,EAAkC,EAAE;IAC/F,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;IAC5D,OAAO;QACL,UAAU,EAAE,GAAG;QACf,IAAI,EAAE,YAAY,OAAO,EAAE;KAC5B,CAAA;AACL,CAAC,CAAA;AANY,QAAA,aAAa,iBAMzB"}

launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "attach",
            "name": "attach Program",
            "port": 5678,
            "address": "localhost",
            "localRoot": "${workspaceFolder}/hello-world/dist",
            "remoteRoot": "/var/task",
            "protocol": "inspector",
            "sourceMaps": true,
            "smartStep": true,
            "outFiles": ["${workspaceFolder}/hello-world/dist"]
        }
    ]
}

As you can see:

  • My lambda function is defined in the hello-world/src/app.ts
  • It is complied with commonJs and ES2020 target to hello-world/dist/app.js with sourcemap
  • The template exposes that handler which in located under the hello-world/dist via the localhost:5000/hello endpoint
  • The debugger is listening on the port 5678

So, when I call npm run start then it prints the following output:

> [email protected] start C:\temp\AWS\LambdaWithApiGateWayDebug\hello-world
> sam local start-api -t ../template.yaml -p 5000 -d 5678

Mounting HelloWorldFunction at http://127.0.0.1:5000/hello [GET]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2020-12-08 11:40:48  * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

When I make a request against the endpoint via Postman then the console is extended with the following text:

Mounting C:\temp\AWS\LambdaWithApiGateWayDebug\hello-world\dist as /var/task:ro,delegated inside runtime container
START RequestId: 04d884cf-fa96-4d58-b41c-e4196e12de13 Version: $LATEST
Debugger listening on ws://0.0.0.0:5678/d6702717-f291-42cd-8056-22b9f029f4dd
For help, see: https://nodejs.org/en/docs/inspector

When I attach my VS Code to the node process then I can only debug the app.js and not the app.ts.
End of the console log:

Debugger attached.
END RequestId: 04d884cf-fa96-4d58-b41c-e4196e12de13
REPORT RequestId: 04d884cf-fa96-4d58-b41c-e4196e12de13  Init Duration: 0.12 ms  Duration: 7064.19 ms    Billed Duration: 7100 ms        Memory Size: 128 MB     Max Memory Used: 128 MB
No Content-Type given. Defaulting to 'application/json'.
2020-12-08 11:40:58 127.0.0.1 - - [08/Dec/2020 11:40:58] "GET /hello HTTP/1.1" 200 -

Question

What should I change to be able to debug my app.ts instead of app.js?

Admittedly answered 8/12, 2020 at 10:55 Comment(1)
I have the same question, until now I only can debug the javascript code, trying to debug figure out a solution. I will let you knowContumacious
A
3

As it turned out I have made two tiny mistakes:

sourceRoot

I've set sourceRoot inside tsconfig.json which is unnecessary in this case. include is enough:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "./dist",
    "strict": true,
    "noImplicitAny": true,
    "esModuleInterop": true,
    // "sourceRoot": "./src",
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

BreakPoints

I've set two breakpoints: one inside app.js and another inside app.ts.
As it turned out if app.js does have a breakpoint then the other one will not be trigger.

debugging js

So after I've removed the breakpoint from app.js then the debugger stopped inside the app.ts.

debugging ts

Admittedly answered 1/2, 2021 at 17:5 Comment(0)
C
6

I wrote a medium article explaining how to create, invoke and debug a TypeScript lambda function. Please for more details check the following link.

Requirements

1- Create a nodejs12x;

  1. Create a nodejs12x lambda;

  2. Install TypeScript: npm i -g typescript

  3. Initialize typescript: tsc --init (It will create the tsconfig.json file)

  4. Replace the created tsconfig.json file with the following code:

    {
    "compilerOptions": {
    "module": "CommonJS",
    "target": "ES2017",
    "noImplicitAny": true,
    "preserveConstEnums": true,
    "outDir": "./built",
    "sourceMap": true
    },
    "include": ["handler/**/*"],
    "exclude": ["node_modules", "**/*.spec.ts"]
    }
    
  5. Delete app.js file;

  6. Create your lambda in TypeScript code inside of handler folder (you need o create it):

    import { 
     APIGatewayProxyEvent, 
     APIGatewayProxyResult 
    } from "aws-lambda";
    
    export const lambdaHandler = async (
      event: APIGatewayProxyEvent
    ): Promise<APIGatewayProxyResult> => {
      const queries = JSON.stringify(event.queryStringParameters);
      return {
        statusCode: 200,
        body: `Queries: ${queries}`
      }
    }
    
  7. Adapt the template.yaml. Change the CodeUri path of your lambda: CodeUri: hello-world/built

  8. Install the needed node packages: npm install typescript @types/aws-lambda @types/node -save-dev

  9. Package json:

    {
    "name": "typescript_lambda",
    "version": "1.0.0",
    "description": "hello world sample for TypeScript",
    "main": "app.js",
    "repository": "https://github.com/jafreitas90/AWS",
    "author": "Jorge Freitas",
    "license": "JorgeFreitas Ltd :)",
    "dependencies": {
    },
    "scripts": {
      "compile": "tsc"
    },
    "devDependencies": {
      "@types/aws-lambda": "^8.10.71",
      "@types/node": "^14.14.22",
      "aws-sdk": "^2.815.0",
      "typescript": "^4.1.3"
    }
    }
    
  10. npm install

  11. npm run compile

  12. Set a breakpoint in your lambda function (TypeScript code). On the left panel select Debug and then Start debugging (green button on top). And that's it :)

  13. Project structure

enter image description here

Please find more details in my tutorial.

source code

Contumacious answered 31/1, 2021 at 22:42 Comment(3)
I've tried your proposed solution, what you have described in your article. Your sample does work as expected (I can debug app.ts) on my local machine but I could not apply to my project (yet). I will try several other ways to achieve it. I'll keep you up-to-date.Admittedly
As it turned out I had two minor mistakes. (Read my post if you are interested about the details). Now I'm able to attach a node inspector without the need to use aws-sam. This is a bit more flexible approach than yours because it is not tight to a specific lambda or endpoint (and its payload). So it's easier to test it multiple times with different payloads or simply call a different api endpoint without the need to launch a new debugger.Admittedly
As you can see in my article at the end of it, I am presenting two solutions: using aws-sam and another one using " Attach to SAM CLI"; so you don't need to use aws-sam if you don't wantContumacious
A
3

As it turned out I have made two tiny mistakes:

sourceRoot

I've set sourceRoot inside tsconfig.json which is unnecessary in this case. include is enough:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "./dist",
    "strict": true,
    "noImplicitAny": true,
    "esModuleInterop": true,
    // "sourceRoot": "./src",
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

BreakPoints

I've set two breakpoints: one inside app.js and another inside app.ts.
As it turned out if app.js does have a breakpoint then the other one will not be trigger.

debugging js

So after I've removed the breakpoint from app.js then the debugger stopped inside the app.ts.

debugging ts

Admittedly answered 1/2, 2021 at 17:5 Comment(0)
D
-3

Your Lambda's runtime is JavaScript, yet you write your code in TypeScript. When you use TypeScript, behind the scenes the compiler is converting your code to JavaScript which is the debuggable code.

In another words, you code your lambda in TS, complier then converts it to JS which at the end is the code that gets executed and can be debugged.

Discordance answered 8/12, 2020 at 13:12 Comment(2)
Yep that's true, but with sourceMap the debugger should be able to track back the JS code's given line to the TS code's related line. That's why I specified in the launch.json with the "sourceMaps": true flag.Admittedly
Humm, I see what you mean. What we end up doing is debugging the JS code in VS Code and from there we go back to the TS file to make changes. I am guessing this is what you are trying to avoid?Discordance

© 2022 - 2024 — McMap. All rights reserved.