You have a compiled TypeScript handler and an execution role with logging permissions. Now you need to get the code into Lambda and verify it works. The workflow is: compile TypeScript, zip the output, create the function (or update it), invoke it, and read the results. All of this happens through the CLI.
If you want AWS’s exact command surface while you work, keep the aws lambda create-function command reference, the aws lambda update-function-code command reference, and the aws lambda invoke command reference open.
Building the Deployment Package
Lambda expects a zip file containing your compiled JavaScript and any dependencies. Start by compiling your TypeScript and creating the zip:
cd lambda
npm run buildThis produces dist/handler.js (and associated source maps). Now zip the contents of the dist/ directory:
cd dist
zip -r ../function.zip .
cd ..The zip file must contain your handler file at the root level—not nested inside a dist/ folder. That’s why you cd into dist/ before zipping: Lambda looks for the handler at the path you specify (e.g., handler.handler), and if your file is at dist/handler.js inside the zip, Lambda won’t find it.
If your function uses node_modules dependencies (beyond what the Lambda runtime provides), you need to include them in the zip. Copy node_modules into the dist/ directory before zipping. For this lesson, the handler has no runtime dependencies—@types/aws-lambda is a dev dependency that’s only used during compilation.
Creating the Function
Use aws lambda create-function to deploy your function for the first time:
aws lambda create-function \
--function-name my-frontend-app-api \
--runtime nodejs20.x \
--role arn:aws:iam::123456789012:role/my-frontend-app-lambda-role \
--handler handler.handler \
--zip-file fileb://function.zip \
--region us-east-1 \
--output jsonLet’s unpack these flags:
--function-name: The name of your function. This is how you reference it everywhere in AWS.--runtime: The language runtime.nodejs20.xgives you Node.js 20 with the AWS SDK v3 pre-installed.--role: The ARN of the execution role you created in Lambda Execution Roles and Permissions.--handler: The path to your handler function in the format<filename>.<export>. Since your compiled file ishandler.jsand the exported function ishandler, this ishandler.handler. (Yes,handler.handlerlooks redundant. You get used to it.)--zip-file: The deployment package. Thefileb://prefix tells the CLI to read the file as binary data.
The response confirms that the function was created:
{
"FunctionName": "my-frontend-app-api",
"FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:my-frontend-app-api",
"Runtime": "nodejs20.x",
"Role": "arn:aws:iam::123456789012:role/my-frontend-app-lambda-role",
"Handler": "handler.handler",
"CodeSize": 1234,
"Timeout": 3,
"MemorySize": 128,
"State": "Active"
}Notice the defaults: 3-second timeout and 128 MB of memory. These are fine for a simple API handler. You can change them later with aws lambda update-function-configuration.
In the console, the Create function page lets you select the runtime and configure basic settings before deploying.

After creation, the Code tab shows the deployed handler code in the inline editor.

Invoking the Function
Now test it. The aws lambda invoke command calls your function directly, without going through API Gateway:
aws lambda invoke \
--function-name my-frontend-app-api \
--cli-binary-format raw-in-base64-out \
--payload '{"requestContext": {"http": {"method": "GET", "path": "/"}}, "queryStringParameters": {"name": "AWS"}}' \
--region us-east-1 \
--output json \
response.jsonThis command does two things: it invokes the function and writes the function’s return value to response.json. The terminal output shows metadata about the invocation:
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}The actual response from your handler is in response.json:
cat response.json{
"statusCode": 200,
"headers": { "Content-Type": "application/json" },
"body": "{\"greeting\":\"Hello, AWS!\",\"timestamp\":\"2026-03-18T12:00:00.000Z\"}"
}Your function received the event, read the name query parameter, and returned a greeting. It works.
In the console, the Test tab lets you configure a test event and run the function directly. The Event JSON editor accepts any JSON payload, and the execution result shows the response and logs inline.

After clicking Test, the result expands above the event editor showing the status, response body, and log output.

The --cli-binary-format raw-in-base64-out flag is required when passing --payload with AWS CLI v2. Without it, the CLI tries to base64-encode your payload, which isn’t what you want for JSON test events.
Using Test Event Files
Typing JSON payloads on the command line gets unwieldy fast. Save your test event to a file instead:
{
"requestContext": {
"http": {
"method": "GET",
"path": "/greeting"
}
},
"queryStringParameters": {
"name": "Frontend Engineer"
},
"headers": {
"content-type": "application/json"
}
}Save this as test-event.json and invoke with:
aws lambda invoke \
--function-name my-frontend-app-api \
--cli-binary-format raw-in-base64-out \
--payload file://test-event.json \
--region us-east-1 \
--output json \
response.jsonUsing file:// (not fileb://—payloads are text, not binary) loads the event from a file. Keep a collection of test events in your project for different scenarios: missing parameters, POST requests, malformed JSON bodies.
Updating the Function Code
You won’t create a new function every time you change your code. After the initial create-function, use update-function-code to deploy changes:
cd lambda
npm run build
cd dist && zip -r ../function.zip . && cd ..
aws lambda update-function-code \
--function-name my-frontend-app-api \
--zip-file fileb://function.zip \
--region us-east-1 \
--output jsonThis replaces the function’s code while preserving all configuration (runtime, memory, timeout, environment variables, execution role). The update is atomic—Lambda serves the old code until the new code is ready, then switches over.
Reading the Logs
Your function’s console.log output goes to CloudWatch Logs. After invoking your function, you can pull the most recent logs with the CLI:
aws logs get-log-events \
--log-group-name /aws/lambda/my-frontend-app-api \
--log-stream-name "$(aws logs describe-log-streams \
--log-group-name /aws/lambda/my-frontend-app-api \
--order-by LastEventTime \
--descending \
--limit 1 \
--query 'logStreams[0].logStreamName' \
--output text \
--region us-east-1)" \
--region us-east-1 \
--output jsonThat nested command finds the most recent log stream and then fetches its events. The output includes your console.log messages, the start and end of each invocation, and the billed duration.
We’ll cover CloudWatch in depth in the CloudWatch section. For now, this command is your debugging tool.
You can also include --log-type Tail on your aws lambda invoke command. This returns the last 4 KB of log output as a base64-encoded string in the response, which you can decode with base64 --decode. It’s quicker than querying CloudWatch for simple debugging.
The Full Deployment Script
Here’s the complete build-and-deploy workflow as a single script. You’ll run some version of this every time you update your function:
# Build
cd lambda
npm run build
# Package
cd dist
zip -r ../function.zip .
cd ..
# Deploy (use create-function the first time, update-function-code after that)
aws lambda update-function-code \
--function-name my-frontend-app-api \
--zip-file fileb://function.zip \
--region us-east-1 \
--output json
# Test
aws lambda invoke \
--function-name my-frontend-app-api \
--cli-binary-format raw-in-base64-out \
--payload file://test-event.json \
--region us-east-1 \
--output json \
response.json
cat response.jsonThis is the manual version. Later in the course, you’ll automate this in a GitHub Actions workflow, just like you automated static frontend deployments in the deployment section.
Common Mistakes
Wrong handler path. If you get “Runtime.HandlerNotFound” when invoking, your --handler value doesn’t match the file and export name inside the zip. Double-check that handler.js is at the root of the zip (not in a subdirectory) and that the exported function is named handler.
Zip contains the directory instead of the contents. If you run zip -r function.zip dist/, the zip contains a dist/ folder, and Lambda looks for dist/handler.handler instead of handler.handler. Always cd into the directory and zip the contents.
Role not propagated. If create-function fails with “The role defined for the function cannot be assumed by Lambda,” wait 10-15 seconds and try again. IAM role propagation is eventually consistent.
Your function is deployed and you can invoke it directly. But direct invocation isn’t how a frontend calls an API. You need an HTTP endpoint. Before you get to the API Gateway section, there are two more Lambda topics to cover: environment variables, for configuration that shouldn’t be hardcoded, and cold starts, the performance characteristic that matters most for API latency.