Moving an endpoint from a traditional Node.js backend to AWS Lambda sounds straightforward.
Take the business logic, put it inside a Lambda function, connect API Gateway, and you're done.
In practice, the architecture changes more than the deployment model.
A traditional Express application is designed around a continuously running server.
Lambda is designed around short-lived function executions.
That difference affects application structure, database connections, configuration, authentication, logging, and performance.
Coming from a Node.js + Express background, these are the things I would pay attention to when migrating an endpoint to Lambda.
Why migrate only one endpoint?
I wouldn't start by moving an entire monolith to serverless.
That's a much bigger risk.
A better approach is to select one endpoint and use it as a migration experiment.
A good candidate could have:
- Relatively independent business logic
- Predictable input/output
- Limited dependencies on the rest of the monolith
- No requirement for a long-running process
- A workload that can benefit from independent scaling
The architecture could change from:
Before
Client
↓
Nginx
↓
EC2
↓
Node.js + Express
↓
MongoDB
to:
After
Client
↓
API Gateway
↓
Lambda
↓
MongoDB
The rest of the application can continue running on EC2 while the selected endpoint is handled by Lambda.
The migration plan
I would break the migration into small steps.
Step 1: Identify the endpoint
First, understand exactly what the endpoint does.
For example:
POST /api/orders
The endpoint might:
- Validate the request
- Authenticate the user
- Read data
- Perform business logic
- Write to MongoDB
- Return a response
Before moving it, I would identify every dependency.
Endpoint
│
├── Authentication
├── Validation
├── Database
├── Services
├── Environment variables
└── External APIs
This makes hidden dependencies visible.
Step 2: Separate business logic from Express
One of the biggest advantages of separating application logic from the HTTP framework is that the business logic becomes easier to reuse.
Instead of putting everything inside:
app.post("/orders", async (req, res) => {
// everything here
});
I would separate the business logic:
const createOrder = async (userId, data) => {
// business logic
};
Then Express can call it:
app.post("/orders", async (req, res) => {
const order = await createOrder(
req.user.id,
req.body
);
res.json(order);
});
And a Lambda handler can call the same logic:
export const handler = async (event) => {
const body = JSON.parse(event.body);
const order = await createOrder(
event.requestContext.authorizer.userId,
body
);
return {
statusCode: 200,
body: JSON.stringify(order)
};
};
The important idea is:
The business logic shouldn't depend heavily on Express.
Step 3: Add API Gateway
The next layer is API Gateway.
The request flow becomes:
Client
↓
API Gateway
↓
Lambda
↓
Business Logic
↓
MongoDB
API Gateway becomes responsible for receiving the HTTP request and invoking the Lambda function.
Depending on the architecture, authentication and authorization can also be handled through appropriate API Gateway/Lambda mechanisms.
What can break?
This is where serverless migrations become interesting.
Problem 1: Database connections
This is one of the first things I'd investigate.
With an Express application on EC2, the Node.js process stays alive.
A MongoDB connection can remain available between requests.
Lambda works differently.
A Lambda execution environment can be created, reused, or terminated.
If every invocation creates a new database connection, a high number of requests can create unnecessary connection pressure.
A naive pattern might look like:
export const handler = async (event) => {
const db = await mongoose.connect(process.env.MONGODB_URI);
// process request
};
Doing this without considering connection reuse can become problematic.
A better pattern is to cache the connection/promise at the module level so that warm Lambda environments can reuse it.
The important lesson is:
Don't treat Lambda like a traditional always-running Express server.
Problem 2: Cold starts
Another difference is cold-start latency.
When AWS needs a new execution environment, Lambda has to initialize the runtime and application code.
That can add latency to some requests.
The simplified flow is:
Request
↓
New Lambda environment
↓
Initialize runtime
↓
Load application
↓
Connect / initialize dependencies
↓
Execute handler
A warm invocation can be much simpler:
Request
↓
Existing environment
↓
Execute handler
This means application startup time matters.
Large dependency trees and heavy initialization can make cold starts worse.
Problem 3: Environment variables
A traditional application may have environment configuration such as:
MONGODB_URI
JWT_SECRET
API_KEY
NODE_ENV
When moving the endpoint to Lambda, those configuration values need to be available to the function.
I would avoid putting sensitive values directly into source code.
The deployment architecture should separate:
Code
+
Configuration
+
Secrets
This becomes especially important when the same function has different configurations for development, staging, and production.
Problem 4: Express assumptions
A monolithic Express application has access to things such as:
req
res
middleware
next()
Lambda doesn't naturally work with that same request lifecycle.
Instead, the handler receives an event:
export const handler = async (event) => {
// event
};
That means Express middleware cannot always be moved directly without modification.
Authentication, validation, error handling, and request parsing may need to be adapted.
This is one reason I would avoid simply copying an Express controller into Lambda and expecting everything to work.
Problem 5: Logging changes
With an EC2-based application, I might inspect application logs using tools such as PM2:
pm2 logs
With Lambda, logging is centered around CloudWatch Logs.
For example:
console.log("Processing order", orderId);
can be captured by CloudWatch.
This changes the operational workflow.
Instead of thinking:
SSH → server → PM2 logs
the workflow becomes more like:
Lambda
↓
CloudWatch Logs
↓
Metrics / Logs / Alarms
For a production migration, I'd make sure observability is ready before moving traffic.
How I would reduce migration risk
I wouldn't immediately switch 100% of production traffic.
A safer approach is:
Existing Endpoint
│
├────────→ EC2 / Express
│
└────────→ Lambda
Depending on the routing strategy, traffic can be moved gradually.
The migration should have a clear rollback path.
If Lambda introduces unexpected latency or errors, the original endpoint should remain available until confidence is established.
Testing before production
Before migrating traffic, I'd test more than just whether the Lambda returns HTTP 200.
I'd test:
[ ] Authentication
[ ] Validation
[ ] Database operations
[ ] Error handling
[ ] External API calls
[ ] Environment variables
[ ] Cold-start behavior
[ ] Concurrent requests
[ ] Database connection behavior
[ ] Response latency
I'd also test the endpoint using realistic payload sizes and data volumes.
A function that works perfectly with one request isn't necessarily ready for production.
Is Lambda always better?
No.
This is probably the most important lesson.
Lambda can be a great fit for certain workloads, but migrating an endpoint simply because:
"Serverless is modern"
isn't a good architectural reason.
A continuously running Express service may still be a better choice when:
- The workload is consistently high
- The application has long-running processes
- The architecture is tightly coupled
- Existing infrastructure is already efficient
- The team benefits from the traditional server model
Lambda becomes more attractive when the workload is well suited to event-driven execution and we want AWS to handle more of the underlying infrastructure.
What I would measure
Before and after migration, I'd compare:
| Metric | Before | After |
|---|---|---|
| Average latency | Measure | Measure |
| p95 latency | Measure | Measure |
| Error rate | Measure | Measure |
| Cold-start latency | N/A | Measure |
| Requests/sec | Measure | Measure |
| Database connections | Measure | Measure |
| Infrastructure overhead | Measure | Measure |
The point isn't to prove Lambda is better.
The point is to determine whether this particular endpoint benefits from the migration.
Was it worth it?
For me, the answer would depend entirely on the measurements.
If the migration provides:
- Better scalability
- Lower operational overhead
- Appropriate latency
- Reliable database behavior
- Reasonable cost
then it can be a good architectural decision.
But if the endpoint becomes significantly more complicated without providing meaningful benefits, keeping it on the existing infrastructure may be the better decision.
Final takeaway
The biggest lesson I take from a Lambda migration is:
Moving code to Lambda isn't the same as designing for serverless.
A successful migration requires thinking about:
Runtime
Database connections
Cold starts
Configuration
Authentication
Observability
Scaling
Failure handling
The best approach isn't to rewrite everything at once.
Start with one suitable endpoint.
Measure its behavior.
Learn from the migration.
Then decide whether the pattern makes sense for the rest of the system.
Good architecture isn't about moving everything to the newest technology.
It's about choosing the right architecture for the workload.
Top comments (0)