Intermediate 25 min

The Key Difference

Express (local): Server runs continuously, waiting for requests.

Lambda (cloud): Function runs only when called. No server to manage.

Understanding Lambda Handlers

A Lambda handler is just a function that AWS calls when a request comes in.

Basic structure:

exports.handler = async (event) => {
  // event contains request data
  // process the request
  return {
    statusCode: 200,
    body: JSON.stringify({ message: "Hello" })
  };
};

The event object: Contains HTTP request data (method, path, headers, body).

The response: Must have statusCode and body (as a string).

Let’s rewrite our API as a pure Lambda handler. This helps you understand how Lambda works.

Create a new file lambda-handler.js:

// In-memory storage
let notes = [
  { id: 1, text: 'Buy milk' },
  { id: 2, text: 'Call mom' }
];
let nextId = 3;

exports.handler = async (event) => {
  // Parse the path and method from the event
  const path = event.path || event.requestContext?.path || '';
  const method = event.httpMethod || event.requestContext?.httpMethod || 'GET';
  
  // Health check
  if (path === '/health' && method === 'GET') {
    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        status: 'ok',
        timestamp: new Date().toISOString()
      })
    };
  }
  
  // Get all notes
  if (path === '/notes' && method === 'GET') {
    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ notes })
    };
  }
  
  // Add a new note
  if (path === '/notes' && method === 'POST') {
    let body;
    try {
      body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body;
    } catch (e) {
      return {
        statusCode: 400,
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ error: 'Invalid JSON' })
      };
    }
    
    const { text } = body;
    if (!text) {
      return {
        statusCode: 400,
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ error: 'text is required' })
      };
    }
    
    const newNote = {
      id: nextId++,
      text: text
    };
    
    notes.push(newNote);
    
    return {
      statusCode: 201,
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(newNote)
    };
  }
  
  // 404 for unknown routes
  return {
    statusCode: 404,
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ error: 'Not found' })
  };
};

Understanding the Lambda Event

The event object structure depends on how API Gateway is configured. For HTTP APIs, it looks like:

{
  "version": "2.0",
  "routeKey": "GET /notes",
  "rawPath": "/notes",
  "rawQueryString": "",
  "headers": {
    "accept": "application/json",
    "content-type": "application/json"
  },
  "requestContext": {
    "http": {
      "method": "GET",
      "path": "/notes"
    }
  },
  "body": null  // or JSON string for POST
}

Our code handles both old and new event formats for compatibility.

Option B: Use aws-serverless-express (Optional)

If you want to keep using Express, you can use aws-serverless-express. This wraps your Express app so it works with Lambda.

Install:

npm install aws-serverless-express

Modify your code:

const express = require('express');
const awsServerlessExpress = require('aws-serverless-express');

const app = express();
app.use(express.json());

// ... your routes ...

// Export the handler
const server = awsServerlessExpress.createServer(app);
exports.handler = (event, context) => {
  awsServerlessExpress.proxy(server, event, context);
};

Pros: Reuse existing Express code.

Cons: Adds complexity, larger package size, harder to debug.

For this tutorial: We’ll use Option A (pure Lambda handler) so you understand how Lambda works.

Side-by-Side Comparison

Express Version (Local)

app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

app.listen(3000);

What happens:

  • Server starts and waits
  • When request comes, Express calls your route handler
  • You use res.json() to send response

Lambda Version (Cloud)

exports.handler = async (event) => {
  if (event.path === '/health') {
    return {
      statusCode: 200,
      body: JSON.stringify({ status: 'ok' })
    };
  }
};

What happens:

  • No server running
  • When request comes, AWS calls your handler
  • You return a response object
  • AWS sends it back to the client

Key Differences

ExpressLambda
app.listen() starts serverNo server to start
req and res objectsevent object
res.json() sends responseReturn response object
Runs continuouslyRuns on-demand
You manage the processAWS manages execution

Testing the Lambda Handler Locally

You can test Lambda handlers locally by creating a test event:

Create test-event.json:

{
  "path": "/notes",
  "httpMethod": "GET",
  "headers": {},
  "body": null
}

Create test-local.js:

const handler = require('./lambda-handler');

const event = require('./test-event.json');

handler.handler(event)
  .then(response => {
    console.log('Response:', JSON.stringify(response, null, 2));
  })
  .catch(error => {
    console.error('Error:', error);
  });

Run:

node test-local.js

This lets you test Lambda code without deploying.

Key Takeaways

Before deploying:

  1. Lambda handlers are just functions - No server, just code that runs on demand
  2. Event object contains request data - Path, method, headers, body
  3. Response must be an object - With statusCode and body (as string)
  4. You can test locally - Create test events to debug
  5. Pure Lambda vs Express wrapper - Pure Lambda is simpler to understand

What’s Next?

In the next page, we’ll package this code and deploy it to AWS. You’ll create a Lambda function, upload the code, and connect it to API Gateway.