Serverless Framework: From Zero to Production


Introduction





The Serverless Framework provides a unified experience for deploying functions, APIs, and event-driven architectures across major cloud providers. While serverless eliminates infrastructure management, it introduces challenges around cold starts, observability, and cost control. This guide walks through taking a serverless application from development to production using the Serverless Framework on AWS Lambda.





Project Setup and Structure





A well-structured serverless project separates concerns across functions, layers, and configuration:






# serverless.yml


service: order-processor


frameworkVersion: "4"




provider:


name: aws


runtime: nodejs20.x


region: us-east-1


stage: ${opt:stage, 'dev'}


environment:


ORDER_TABLE: ${self:custom.tableName}


QUEUE_URL: !Ref OrderQueue




plugins:


- serverless-webpack


- serverless-offline


- serverless-prune-plugin




custom:


tableName: orders-${self:provider.stage}


webpack:


packager: pnpm


excludeFiles: src/**/*.test.ts


prune:


automatic: true


number: 3




functions:


createOrder:


handler: src/handlers/createOrder.handler


events:


- httpApi:


method: POST


path: /orders


timeout: 10


memorySize: 256


iamRoleStatements:


- Effect: Allow


Action: dynamodb:PutItem


Resource: !GetAtt OrdersTable.Arn







Infrastructure as Code





Define resources alongside functions for self-documenting infrastructure:






resources:


Resources:


OrdersTable:


Type: AWS::DynamoDB::Table


Properties:


TableName: orders-${self:provider.stage}


BillingMode: PAY_PER_REQUEST


AttributeDefinitions:


- AttributeName: orderId


AttributeType: S


- AttributeName: status


AttributeType: S


KeySchema:


- AttributeName: orderId


KeyType: HASH


GlobalSecondaryIndexes:


- IndexName: StatusIndex


KeySchema:


- AttributeName: status


KeyType: HASH


Projection:


ProjectionType: ALL


OrderQueue:


Type: AWS::SQS::Queue


Properties:


QueueName: orders-${self:provider.stage}


VisibilityTimeout: 60


RedrivePolicy:


deadLetterTargetArn: !GetAtt DeadLetterQueue.Arn


maxReceiveCount: 3


DeadLetterQueue:


Type: AWS::SQS::Queue


Properties:


QueueName: orders-dlq-${self:provider.stage}







Lambda Handler Implementation





Write handlers with proper error handling and observability:






// src/handlers/createOrder.ts


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


import { DynamoDBClient } from "@aws-sdk/client-dynamodb";


import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";


import { randomUUID } from "crypto";


import { Logger } from "@aws-lambda-powertools/logger";


import { Metrics } from "@aws-lambda-powertools/metrics";


import { Tracer } from "@aws-lambda-powertools/tracer";




const logger = new Logger({ serviceName: "order-processor" });


const metrics = new Metrics({ namespace: "OrderProcessor" });


const tracer = new Tracer({ serviceName: "order-processor" });




const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));




export const handler = async (


event: APIGatewayProxyEvent


): Promise => {


try {


const body = JSON.parse(event.body || "{}");


const orderId = randomUUID();


const order = {


orderId,


...body,


status: "PENDING",


createdAt: new Date().toISOString(),


};




await ddb.send(new PutCommand({


TableName: process.env.ORDER_TABLE,


Item: order,


}));




metrics.addMetric("OrderCreated", 1, "Count");


logger.info("Order created", { orderId });




return {


statusCode: 201,


headers: { "Content-Type": "application/json" },


body: JSON.stringify({ orderId, status: "PENDING" }),


};


} catch (error) {


logger.error("Failed to create order", { error });


metrics.addMetric("OrderCreationError", 1, "Count");


return {


statusCode: 500,


body: JSON.stringify({ message: "Internal server error" }),


};


}


};







Local Development





The `serverless-offline` plugin provides a local Lambda emulator:






# Start local API Gateway emulator


serverless offline --stage dev --httpPort 4000




# Invoke a function directly


serverless invoke local --function createOrder \


--path test/fixtures/create-order.json




# Run with warm container simulation


serverless offline --stage dev \


--noPrependStageInUrl \


--reloadHandler







Cold Start Optimization





Cold starts add latency when Lambda scales up a new execution environment:






# Optimize for cold starts


provider:


# Use AWS Graviton for better price/performance


architecture: arm64


# Increase memory speeds up CPU allocation


# (and proportionally reduces cold start time)


memorySize: 1024




functions:


latencyCritical:


handler: src/handlers/critical.handler


# Provisioned concurrency for critical paths


provisionedConcurrency: 5


# Reserve concurrency to prevent throttling


reservedConcurrency: 20







Code-level optimizations:






// Cold start optimization techniques




// 1. Lazy initialization outside handler (reused across invocations)


let client: DynamoDBDocumentClient;


function getClient(): DynamoDBDocumentClient {


if (!client) {


client = DynamoDBDocumentClient.from(new DynamoDBClient({}));


}


return client;


}




// 2. Bundle and tree-shake dependencies


// Use esbuild or webpack to exclude unused SDK clients




// 3. Minimize deployment package size


// Exclude in webpack:


// externals: ["@aws-sdk/client-dynamodb"]




// 4. Use AWS SDK v3 for modular imports


import { DynamoDBClient } from "@aws-sdk/client-dynamodb";


// Instead of: import { DynamoDB } from "aws-sdk";







Monitoring and Observability





CloudWatch is the default, but Powertools enhances observability significantly:






functions:


processOrder:


handler: src/handlers/processOrder.handler


environment:


POWERTOOLS_SERVICE_NAME: order-processor


POWERTOOLS_METRICS_NAMESPACE: OrderProcessor


LOG_LEVEL: INFO


events:


- sqs:


arn: !GetAtt OrderQueue.Arn


batchSize: 10


maximumBatchingWindowInSeconds: 5







Create CloudWatch dashboards and alarms:






resources:


Resources:


OrderErrorAlarm:


Type: AWS::CloudWatch::Alarm


Properties:


AlarmName: order-processor-errors-${self:provider.stage}


MetricName: Errors


Namespace: AWS/Lambda


Dimensions:


- Name: FunctionName


Value: !Ref ProcessOrderLambdaFunction


Statistic: Sum


Period: 300


EvaluationPeriods: 1


Threshold: 5


ComparisonOperator: GreaterThanThreshold







Cost Analysis





Serverless costs are driven by invocation count, duration, and memory allocation:






# Calculate monthly cost estimate


# Invocations: 10M/month


# Avg duration: 200ms


# Memory: 1024MB


# Cost = 10M * (0.2s / 1000ms) * 1GB * $0.00001667/GB-second


# = 10M * 0.2 * 0.00001667


# = ~$33.34/month







Optimize cost with `serverless-prune-plugin` to remove old versions:






custom:


prune:


automatic: true


number: 3 # Keep only 3 latest versions







For high-throughput workloads, compare Lambda cost against ECS Fargate at sustained traffic levels. Lambda often wins for variable, low-volume traffic but becomes expensive at steady-state high throughput. Use these patterns to build production-grade serverless applications that are cost-effective, observable, and performant from day one.