Serverless Architecture in 2025: Why Your Next API Should Be Serverless
Hello HaWkers, imagine paying only for the exact milliseconds your code executes, scaling automatically from zero to millions of requests, and never worrying about servers again. Welcome to the reality of serverless in 2025.
Have you ever wondered why companies like Netflix, Coca-Cola, and iRobot migrated critical parts of their infrastructure to serverless? The answer goes beyond costs - it's about agility, scalability, and focusing on what really matters: your code.
What Is Serverless and Why Now?
Serverless doesn't mean "no servers". It means you don't manage servers. The infrastructure is abstracted and managed by the cloud provider, and you only pay for the actual execution time of your code.
In 2025, serverless has reached maturity with:
- Cold start reduced to <50ms
- Native support for containers
- Global edge deployment
- Seamless integration with databases and services
- Costs up to 70% lower than traditional infrastructure
Comparison of Major Providers
AWS Lambda: The Mature Giant
// AWS Lambda with Node.js
export const handler = async (event) => {
const { userId } = JSON.parse(event.body);
// Connection with DynamoDB
const dynamodb = new DynamoDB.DocumentClient();
try {
const user = await dynamodb.get({
TableName: 'Users',
Key: { userId }
}).promise();
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify(user.Item)
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: error.message })
};
}
};
// Configuration (serverless.yml)
service: user-api
provider:
name: aws
runtime: nodejs20.x
region: us-east-1
environment:
DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
functions:
getUser:
handler: handler.handler
events:
- http:
path: users/{userId}
method: get
cors: true
reservedConcurrency: 10 # Limits simultaneous executions
AWS Lambda Advantages:
- Complete AWS ecosystem
- 15-minute timeout (longest)
- Container support
- Pricing: $0.20 per 1M requests + compute
Vercel Functions: Simplicity for Frontend
// api/user/[id].ts (Vercel Functions)
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export const config = {
runtime: 'edge', // or 'nodejs'
};
export default async function handler(req: NextRequest) {
const { searchParams } = new URL(req.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json(
{ error: 'User ID required' },
{ status: 400 }
);
}
try {
const user = await prisma.user.findUnique({
where: { id },
select: {
id: true,
name: true,
email: true,
createdAt: true
}
});
if (!user) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
return NextResponse.json(user);
} catch (error) {
console.error('Database error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
Vercel Advantages:
- Automatic deployment with Git push
- Edge Functions (ultra-low latency)
- Perfect integration with Next.js
- Pricing: Generous free tier, $20/month Pro
Cloudflare Workers: The Fastest
// Cloudflare Worker
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
const userId = url.pathname.split('/').pop();
// KV is Cloudflare's key-value storage
const user = await USER_KV.get(userId, 'json');
if (!user) {
return new Response(
JSON.stringify({ error: 'User not found' }),
{
status: 404,
headers: { 'Content-Type': 'application/json' }
}
);
}
// Runs on global edge locations
return new Response(JSON.stringify(user), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=60'
}
});
}
// Middleware for authentication
async function authenticate(request) {
const token = request.headers.get('Authorization');
if (!token) {
return new Response('Unauthorized', { status: 401 });
}
// Validates JWT on edge
const isValid = await verifyJWT(token);
if (!isValid) {
return new Response('Invalid token', { status: 401 });
}
return null; // Continue
}
Cloudflare Advantages:
- Global latency <10ms
- Virtually zero cold start
- 200+ edge locations
- Pricing: $5/month (10M requests)
Perfect Use Cases for Serverless
1. REST and GraphQL APIs
// Serverless GraphQL API with Apollo
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateLambdaHandler } from '@as-integrations/aws-lambda';
const typeDefs = `
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
}
type Mutation {
createPost(title: String!, content: String!, authorId: ID!): Post!
updatePost(id: ID!, title: String, content: String): Post!
}
`;
const resolvers = {
Query: {
user: async (_, { id }, context) => {
return await context.dataSources.userAPI.getUser(id);
},
users: async (_, __, context) => {
return await context.dataSources.userAPI.getUsers();
},
post: async (_, { id }, context) => {
return await context.dataSources.postAPI.getPost(id);
}
},
Mutation: {
createPost: async (_, args, context) => {
return await context.dataSources.postAPI.createPost(args);
}
},
User: {
posts: async (user, _, context) => {
return await context.dataSources.postAPI.getPostsByAuthor(user.id);
}
}
};
const server = new ApolloServer({
typeDefs,
resolvers
});
export const handler = startServerAndCreateLambdaHandler(server);
2. Batch Data Processing
// Lambda for asynchronous processing with SQS
export const handler = async (event) => {
// Receives messages from SQS
const records = event.Records;
const results = await Promise.all(
records.map(async (record) => {
const message = JSON.parse(record.body);
try {
// Processes image, generates thumbnail, etc
await processImage(message.imageUrl);
// Removes message from queue
return {
itemIdentifier: record.messageId,
batchItemFailures: []
};
} catch (error) {
console.error('Processing failed:', error);
// Message returns to queue
return {
batchItemFailures: [{ itemIdentifier: record.messageId }]
};
}
})
);
return { batchItemFailures: [] };
};
async function processImage(imageUrl) {
// Downloads the image
const response = await fetch(imageUrl);
const buffer = await response.arrayBuffer();
// Resizes using sharp
const thumbnail = await sharp(buffer)
.resize(200, 200, { fit: 'cover' })
.toBuffer();
// Uploads to S3
await s3.putObject({
Bucket: 'thumbnails',
Key: `thumb_${Date.now()}.jpg`,
Body: thumbnail,
ContentType: 'image/jpeg'
});
}
3. Webhooks and Integrations
// Stripe webhook in Vercel Function
import { NextRequest } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export const config = {
api: {
bodyParser: false, // Stripe needs the raw body
},
};
export default async function handler(req: NextRequest) {
if (req.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
const body = await req.text();
const sig = req.headers.get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
} catch (err) {
console.error('Webhook signature verification failed:', err);
return new Response('Webhook error', { status: 400 });
}
// Handle Stripe events
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
await fulfillOrder(paymentIntent.id);
break;
case 'customer.subscription.created':
const subscription = event.data.object;
await activateSubscription(subscription.customer as string);
break;
case 'invoice.payment_failed':
const invoice = event.data.object;
await notifyPaymentFailure(invoice.customer as string);
break;
}
return new Response(JSON.stringify({ received: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
4. Scheduled Jobs (Cron)
// Lambda with EventBridge (Cron)
// Runs every day at 2am to clean old data
export const handler = async () => {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
// Cleans old records
const result = await dynamodb.scan({
TableName: 'Logs',
FilterExpression: 'createdAt < :date',
ExpressionAttributeValues: {
':date': thirtyDaysAgo.toISOString()
}
}).promise();
// Batch delete
const deletePromises = result.Items.map(item =>
dynamodb.delete({
TableName: 'Logs',
Key: { id: item.id }
}).promise()
);
await Promise.all(deletePromises);
console.log(`Deleted ${deletePromises.length} old records`);
// Sends metric to CloudWatch
await cloudwatch.putMetricData({
Namespace: 'Cleanup',
MetricData: [{
MetricName: 'RecordsDeleted',
Value: deletePromises.length,
Unit: 'Count'
}]
}).promise();
return {
statusCode: 200,
body: JSON.stringify({ deleted: deletePromises.length })
};
};
Serverless Best Practices
1. Database Connection Management
// ❌ Bad: Creates new connection on each invocation
export const handler = async (event) => {
const db = await mongoose.connect(process.env.MONGO_URL);
// ... uses db ...
await db.disconnect();
};
// ✅ Good: Reuses connection between invocations
let cachedDb = null;
async function connectToDatabase() {
if (cachedDb) {
return cachedDb;
}
cachedDb = await mongoose.connect(process.env.MONGO_URL, {
bufferCommands: false,
maxPoolSize: 1 // Lambda reuses few connections
});
return cachedDb;
}
export const handler = async (event) => {
const db = await connectToDatabase();
// ... uses db ...
// DOESN'T disconnect - leaves for next invocation
};
2. Cold Start Optimization
// Top-level imports are executed once
import { DynamoDB } from 'aws-sdk';
import { someHeavyLibrary } from 'heavy-lib';
// Initialization outside handler
const dynamodb = new DynamoDB.DocumentClient();
// Minimalist handler
export const handler = async (event) => {
// Lightweight and focused code
const result = await dynamodb.get({
TableName: 'Data',
Key: { id: event.id }
}).promise();
return result.Item;
};
3. Error Handling and Retry
// Implements retry with exponential backoff
async function withRetry(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
export const handler = async (event) => {
try {
const result = await withRetry(async () => {
return await externalAPI.call();
});
return { statusCode: 200, body: JSON.stringify(result) };
} catch (error) {
// Structured logging for CloudWatch
console.error(JSON.stringify({
error: error.message,
stack: error.stack,
event: event
}));
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' })
};
}
};
Costs: Serverless vs Traditional
Real Example: API with 1M req/month
Serverless (AWS Lambda):
- 1M requests: $0.20
- Compute (100ms avg): $1.67
- Total: ~$2/month
Traditional (EC2 t3.small):
- 24/7 instance: $15.18/month
- Load balancer: $16.20/month
- Total: ~$31/month
Savings: 93%
When Serverless Is Not Ideal
- Applications with constant 24/7 traffic - Dedicated server may be cheaper
- Long-duration processing - Lambda has a 15-minute limit
- Complex stateful applications - Long-duration WebSocket
- GPU requirements - Still limited in serverless
The Future of Serverless
In 2025, we're seeing:
- WASM on edge for native performance
- Stateful serverless with Durable Functions
- Simplified multi-cloud
- AI-powered optimization automatically
If you're interested in other modern architectures, I recommend checking out another article: Server-First Development: The Future with Astro, Remix, and SvelteKit where you'll discover how modern frameworks are rethinking web architecture.
Let's go! 🦅
💻 Master JavaScript for Real
The knowledge you gained in this article is just the beginning. There are techniques, patterns, and practices that transform beginner developers into sought-after professionals.
Invest in Your Future
I've prepared complete material for you to master JavaScript:
Payment options:
- 2x of $13.08 no interest
- or $24.90 at sight