Intermediate 25 min

Step 2: Write the Serverless Function

This is the core of our system. The function receives events when files are uploaded and processes them.

Understanding Event Payloads

When a file is uploaded, the storage service sends an event to your function. The event contains information about the file.

Here’s what a typical event looks like:

🟨 JavaScript Event Payload Example
📟 Console Output
Run code to see output...

Function Responsibilities

Your function needs to:

  1. Receive the event - Handler function gets called with event data
  2. Extract file metadata - Get bucket name, file name, size
  3. Format message - Create email content with file details
  4. Send notification - Call email service (we’ll add this in next page)

Function Skeleton

Let’s start with a basic function that just logs the event:

// handler.js
exports.handler = async (event) => {
console.log('Event received:', JSON.stringify(event, null, 2));

// Process each record in the event
for (const record of event.Records) {
  const bucketName = record.s3.bucket.name;
  const objectKey = record.s3.object.key;
  const fileSize = record.s3.object.size;
  
  console.log(`File uploaded: ${objectKey} in bucket ${bucketName}`);
  console.log(`File size: ${fileSize} bytes`);
}

return {
  statusCode: 200,
  body: JSON.stringify({ message: 'Event processed' })
};
};

Parsing the Event

Different cloud providers send events in different formats. Let’s handle each:

// AWS S3 event structure
exports.handler = async (event) => {
for (const record of event.Records) {
  // Extract file information
  const bucketName = record.s3.bucket.name;
  const objectKey = decodeURIComponent(
    record.s3.object.key.replace(/\+/g, ' ')
  );
  const fileSize = record.s3.object.size;
  const eventTime = record.eventTime;
  
  // Get file extension
  const fileExtension = objectKey.split('.').pop() || '';
  
  console.log(`Bucket: ${bucketName}`);
  console.log(`File: ${objectKey}`);
  console.log(`Size: ${fileSize} bytes`);
  console.log(`Type: ${fileExtension}`);
  console.log(`Time: ${eventTime}`);
}

return { statusCode: 200 };
};

Formatting the Message

Now let’s create a formatted message for the email:

// Format email message
function formatEmailMessage(bucketName, objectKey, fileSize, eventTime) {
const fileName = objectKey.split('/').pop();
const fileExtension = fileName.split('.').pop() || 'unknown';
const sizeInMB = (fileSize / (1024 * 1024)).toFixed(2);

const subject = `New file uploaded: ${fileName}`;

const body = `
A new file has been uploaded to your storage bucket.

File Details:
- Name: ${fileName}
- Path: ${objectKey}
- Size: ${sizeInMB} MB (${fileSize} bytes)
- Type: ${fileExtension}
- Bucket: ${bucketName}
- Uploaded: ${new Date(eventTime).toLocaleString()}

You can access this file in your storage bucket.
`;

return { subject, body };
}

// Usage
const message = formatEmailMessage(
'my-notifications-bucket-2025',
'documents/report.pdf',
1024000,
'2025-11-23T10:00:00.000Z'
);

console.log('Subject:', message.subject);
console.log('Body:', message.body);

Complete Function (Before Email Integration)

Here’s the complete function structure. We’ll add email sending in the next page:

// handler.js - Complete function
exports.handler = async (event) => {
console.log('Event received:', JSON.stringify(event, null, 2));

try {
  // Process each record
  for (const record of event.Records) {
    const bucketName = record.s3.bucket.name;
    const objectKey = decodeURIComponent(
      record.s3.object.key.replace(/\+/g, ' ')
    );
    const fileSize = record.s3.object.size;
    const eventTime = record.eventTime;
    
    // Format message
    const message = formatEmailMessage(
      bucketName,
      objectKey,
      fileSize,
      eventTime
    );
    
    console.log('Message prepared:', message.subject);
    
    // TODO: Send email (next page)
    // await sendEmail(message);
  }
  
  return {
    statusCode: 200,
    body: JSON.stringify({ message: 'Processed successfully' })
  };
} catch (error) {
  console.error('Error processing event:', error);
  return {
    statusCode: 500,
    body: JSON.stringify({ error: error.message })
  };
}
};

function formatEmailMessage(bucketName, objectKey, fileSize, eventTime) {
const fileName = objectKey.split('/').pop();
const sizeInMB = (fileSize / (1024 * 1024)).toFixed(2);

return {
  subject: `New file uploaded: ${fileName}`,
  body: `A new file (${fileName}, ${sizeInMB} MB) was uploaded to ${bucketName}.`
};
}

Try It: Predict the Output

Look at this event payload. What will record.s3.object.key contain?

{
  "Records": [{
    "s3": {
      "bucket": { "name": "my-bucket" },
      "object": { "key": "documents%2Freport.pdf", "size": 1024000 }
    }
  }]
}
Answer

The key is URL-encoded: documents%2Freport.pdf which decodes to documents/report.pdf.

The %2F is the encoded form of /. That’s why we use decodeURIComponent() in the code.

Common Bugs to Avoid

Bug 1: Wrong property path

// ❌ Wrong
const fileName = event.s3.object.key;

// ✅ Correct
const fileName = event.Records[0].s3.object.key;

Bug 2: Forgetting URL decoding

// ❌ Wrong (if key has special characters)
const objectKey = record.s3.object.key;

// ✅ Correct
const objectKey = decodeURIComponent(
  record.s3.object.key.replace(/\+/g, ' ')
);

Bug 3: Not handling multiple records

// ❌ Wrong (only processes first record)
const record = event.Records[0];

// ✅ Correct (processes all records)
for (const record of event.Records) {
  // process each
}

What’s Next?

You now have a function that receives events and formats messages. In the next page, we’ll add email sending functionality and connect everything together.