As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!
When I first started working with serverless functions, the promise was simple: write your code, deploy it, and let the cloud handle the rest. No servers to patch, no capacity to guess. It sounded almost too good to be true. In practice, I learned that writing code for this environment requires a different mindset. You're crafting small, event-driven pieces of logic that need to be robust, efficient, and ready to scale from zero to thousands of executions in seconds.
Let's talk about how to write these functions well. I'll share some patterns and techniques I've found essential, using plain JavaScript.
Think of your serverless function as a single, focused task. It should do one thing and do it well. A common mistake is building a function that tries to handle an entire user workflow. Instead, break it down. Have one function validate a user's request, another process the data, and a third send a notification. This makes each piece easier to test, debug, and reuse.
Here’s a basic structure I often begin with. It clearly separates validation, business logic, and response formatting.
export const handler = async (event) => {
// 1. Parse and Validate
const userInput = JSON.parse(event.body || '{}');
if (!userInput.email || !isValidEmail(userInput.email)) {
return formatResponse(400, { error: 'A valid email is required.' });
}
// 2. Execute Core Logic
const userId = await saveUserToDatabase(userInput);
await sendWelcomeEmail(userInput.email);
// 3. Return a Clear Response
return formatResponse(201, {
message: 'User created successfully.',
userId: userId
});
};
// Helper functions keep the handler clean
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function formatResponse(statusCode, body) {
return {
statusCode: statusCode,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
};
}
// These would connect to real services
async function saveUserToDatabase(data) {
// Simulate a database call
return `user_${Date.now()}`;
}
async function sendWelcomeEmail(email) {
// Simulate an email service call
console.log(`Welcome email sent to ${email}`);
}
One of the first surprises you'll encounter is the "cold start." When a function hasn't been called in a while, the platform needs to start a new instance of it, which takes a few hundred milliseconds. For user-facing APIs, this delay matters.
You can minimize its impact. Keep your deployment package lean by only including necessary dependencies. More importantly, initialize your connections to databases or other services outside the main handler function. This code runs once when the instance is created and the connections can be reused for subsequent requests.
// Initialize expensive resources ONCE, during the cold start
const databaseConnection = initializeDatabaseConnection();
const apiClient = new ExternalServiceClient();
let cache = {};
async function initializeDatabaseConnection() {
console.log('Establishing database connection...');
// Simulate a slow connection setup
await new Promise(resolve => setTimeout(resolve, 200));
return { query: () => console.log('Query executed') };
}
export const handler = async (event) => {
// Use the pre-established connection for every request
const result = await databaseConnection.query('SELECT * FROM items');
// Also reuse the initialized API client
const serviceData = await apiClient.fetchData();
return formatResponse(200, { result, serviceData });
};
Your functions don't exist in a vacuum. They are triggered by events. This is the heart of serverless. An image is uploaded to cloud storage, a new record is added to a database, a scheduled timer fires—each of these is an event that can kick off your code.
Here’s how you might handle different types of triggers. The key is to parse the standardized event format from the platform.
// Handler for an HTTP API request (like from AWS API Gateway)
export const httpHandler = async (event) => {
const { httpMethod, path, queryStringParameters, body } = event;
if (httpMethod === 'GET' && path === '/users') {
const users = await getUsers(queryStringParameters);
return formatResponse(200, users);
}
if (httpMethod === 'POST' && path === '/users') {
const newUser = await createUser(JSON.parse(body));
return formatResponse(201, newUser);
}
return formatResponse(404, { error: 'Route not found' });
};
// Handler for a file upload event (like from AWS S3)
export const fileUploadHandler = async (event) => {
for (const record of event.Records) {
const bucketName = record.s3.bucket.name;
const fileName = record.s3.object.key;
console.log(`File ${fileName} was uploaded to ${bucketName}`);
// Here you could resize an image, extract text, etc.
if (fileName.endsWith('.jpg')) {
await processImage(bucketName, fileName);
}
}
return { status: 'File processing initiated' };
};
// Handler for a scheduled or timer-based event
export const scheduledTaskHandler = async (event) => {
console.log('Running scheduled cleanup at:', event.time);
const deletedCount = await cleanupOldRecords();
await generateDailyReport();
return { tasksCompleted: 2, recordsDeleted: deletedCount };
};
Because functions are stateless—they don’t remember anything between runs—any data you need to keep must be stored externally. This is a fundamental shift from traditional applications. You'll rely heavily on databases, object storage, and caching services.
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
const client = new DynamoDBClient({ region: 'us-east-1' });
export const handler = async (event) => {
const user = JSON.parse(event.body);
const userId = `user_${Date.now()}`;
// State is saved to a database
const params = {
TableName: 'Users',
Item: {
userId: { S: userId },
email: { S: user.email },
createdAt: { S: new Date().toISOString() }
}
};
try {
await client.send(new PutItemCommand(params));
return formatResponse(200, { userId, saved: true });
} catch (error) {
console.error('Database error:', error);
return formatResponse(500, { error: 'Failed to save user' });
}
};
In a system where functions are triggered by events from queues, databases, or APIs, things will fail. Network calls time out, third-party services go down, or you might get the same event twice. Planning for this is not optional.
Implementing retry logic with exponential backoff is a standard practice. This means if a request fails, you wait a short time and try again, then wait a bit longer, and so on. It prevents overwhelming a struggling service.
async function callUnreliableService(data, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`Attempt ${attempt} for service call`);
const response = await fetch('https://api.example.com/process', {
method: 'POST',
body: JSON.stringify(data)
});
return await response.json();
} catch (error) {
lastError = error;
if (attempt === maxRetries) break;
// Wait longer after each attempt: 200ms, 400ms, 800ms...
const delay = 200 * Math.pow(2, attempt - 1);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error(`Service call failed after ${maxRetries} attempts: ${lastError.message}`);
}
export const handler = async (event) => {
try {
const result = await callUnreliableService(event.data);
return formatResponse(200, result);
} catch (error) {
// Send the failed event to a "dead letter queue" for later inspection
await sendToErrorQueue(event, error.message);
return formatResponse(502, { error: 'Service temporarily unavailable' });
}
};
Every millisecond of execution time costs money and affects user experience. Performance optimization in serverless is about being efficient with resources.
A major tip is to cache responses when possible. If you're fetching data that doesn't change often, store it in memory for a short period. Just remember that when your function instance is shut down, the cache is lost.
let cachedConfig = null;
let configLastFetched = 0;
const CONFIG_TTL = 300000; // 5 minutes in milliseconds
async function getConfiguration() {
const now = Date.now();
// Return cached config if it's still fresh
if (cachedConfig && (now - configLastFetched) < CONFIG_TTL) {
console.log('Returning cached configuration');
return cachedConfig;
}
// Otherwise, fetch from the source
console.log('Fetching fresh configuration');
const response = await fetch('https://api.example.com/config');
cachedConfig = await response.json();
configLastFetched = now;
return cachedConfig;
}
export const handler = async (event) => {
// This call will be fast on repeated requests within the same function instance
const appConfig = await getConfiguration();
// Use the config in your logic...
return formatResponse(200, { config: appConfig, source: configLastFetched ? 'cache' : 'api' });
};
Security in serverless is about trust and boundaries. Your function should only have the permissions it absolutely needs to do its job. This is called the principle of least privilege. If a function only reads from a database, don't give it permission to delete tables.
Always, always validate and sanitize input. Your function is exposed to the internet, and every piece of data coming in is suspect.
import { sanitize } from 'some-sanitization-library';
export const handler = async (event) => {
const rawInput = JSON.parse(event.body);
// 1. Validate structure and type
if (typeof rawInput.comment !== 'string') {
return formatResponse(400, { error: 'Invalid input type' });
}
// 2. Check for malicious content or excessive length
if (rawInput.comment.length > 1000) {
return formatResponse(400, { error: 'Comment is too long' });
}
// 3. Sanitize the content before using it
const cleanComment = sanitize(rawInput.comment);
// 4. Use parameterized queries to prevent injection
await database.query(
'INSERT INTO comments (text, user_id) VALUES ($1, $2)',
[cleanComment, rawInput.userId] // Parameters are safely separated
);
return formatResponse(200, { status: 'Comment saved' });
};
Finally, you can't manage what you can't see. Good logging is your lifeline when something goes wrong in the cloud. Use structured logging (JSON) so you can easily search and filter logs. Include the function's request ID in every log entry; this lets you trace a single request's journey through all your functions.
export const handler = async (event, context) => {
// The context contains the unique request ID
const requestId = context.awsRequestId || `req_${Date.now()}`;
const logger = {
info: (msg, data) => console.log(JSON.stringify({
level: 'INFO',
requestId,
timestamp: new Date().toISOString(),
message: msg,
data
})),
error: (msg, error) => console.error(JSON.stringify({
level: 'ERROR',
requestId,
timestamp: new Date().toISOString(),
message: msg,
error: error.message,
stack: error.stack
}))
};
logger.info('Function invoked', { path: event.path });
try {
// Your business logic here
const result = await processRequest(event);
logger.info('Function completed successfully', { resultId: result.id });
return formatResponse(200, result);
} catch (err) {
logger.error('Function failed', err);
return formatResponse(500, { error: 'Internal server error', requestId });
}
};
Moving to serverless changed how I think about building software. It pushes you toward small, independent, and resilient pieces of code. You stop worrying about servers and start focusing more on business logic and user experience. It’s not without its challenges—testing, debugging, and monitoring require new tools and approaches. But the payoff in scalability and operational simplicity is significant. Start with a single function, automate a small task, and see how it feels. You might find it’s a surprisingly natural way to build for the modern web.
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva
Top comments (0)