Serverless Architecture in 2025: Building Without Servers
Master serverless architecture with AWS Lambda, Vercel Functions, and Cloudflare Workers. Learn when to go serverless and how to build production-ready applications.
Serverless Architecture in 2025: Building Without Servers
Serverless doesn't mean "no servers"βit means you don't manage them. This paradigm shift allows developers to focus on code while cloud providers handle infrastructure, scaling, and reliability.
What Is Serverless?
Serverless architecture consists of:
- Functions: Code executed on-demand
- Managed services: Databases, storage, queues
- Event-driven: Triggered by events, not always running
- Pay-per-use: Only pay for actual execution time
Core Concepts
1. Functions as a Service (FaaS)
Deploy individual functions, not entire servers:
// AWS Lambda function
export const handler = async (event) => {
const { userId } = JSON.parse(event.body);
const user = await dynamodb.get({
TableName: 'Users',
Key: { id: userId },
});
return {
statusCode: 200,
body: JSON.stringify(user),
};
};
2. Event-Driven Architecture
Functions trigger on events:
// Triggered by S3 upload
export const handler = async (event) => {
const bucket = event.Records[0].s3.bucket.name;
const key = event.Records[0].s3.object.key;
// Process image
const image = await s3.getObject({ Bucket: bucket, Key: key });
const thumbnail = await createThumbnail(image);
// Save thumbnail
await s3.putObject({
Bucket: bucket,
Key: `thumbnails/${key}`,
Body: thumbnail,
});
};
3. Stateless Functions
Each invocation is independent:
// β
Good - stateless
export const handler = async (event) => {
const db = await connectToDatabase(); // Fresh connection
const result = await db.query(event.query);
return result;
};
// β Bad - stateful (unreliable in serverless)
let cache = {};
export const handler = async (event) => {
// Cache may not persist between invocations
if (cache[event.key]) {
return cache[event.key];
}
// ...
};
Popular Serverless Platforms
AWS Lambda
The original serverless platform:
// Lambda with API Gateway
export const handler = async (event) => {
const method = event.httpMethod;
const path = event.path;
if (method === 'GET' && path === '/users') {
const users = await getUsers();
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(users),
};
}
return {
statusCode: 404,
body: JSON.stringify({ error: 'Not found' }),
};
};
Deployment:
# serverless.yml
service: my-api
provider:
name: aws
runtime: nodejs18.x
functions:
api:
handler: handler.handler
events:
- http:
path: users
method: get
Vercel Functions
Optimized for frontend frameworks:
// api/users.js
export default async function handler(req, res) {
if (req.method === 'GET') {
const users = await db.users.findAll();
res.status(200).json(users);
} else {
res.status(405).json({ error: 'Method not allowed' });
}
}
Deploy:
vercel --prod
Cloudflare Workers
Edge-based serverless:
// index.js
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === '/api/users') {
const users = await env.DB.prepare(
'SELECT * FROM users'
).all();
return Response.json(users.results);
}
return new Response('Not found', { status: 404 });
},
};
Azure Functions
Microsoft's serverless offering:
// function.js
module.exports = async function (context, req) {
if (req.method === 'GET') {
const users = await getUsers();
context.res = {
status: 200,
body: users,
};
}
};
Building Serverless APIs
REST API Example
// AWS Lambda + API Gateway
// GET /products
export const listProducts = async () => {
const products = await dynamodb.scan({
TableName: 'Products',
});
return {
statusCode: 200,
body: JSON.stringify(products.Items),
};
};
// GET /products/:id
export const getProduct = async (event) => {
const { id } = event.pathParameters;
const product = await dynamodb.get({
TableName: 'Products',
Key: { id },
});
return {
statusCode: 200,
body: JSON.stringify(product.Item),
};
};
// POST /products
export const createProduct = async (event) => {
const product = JSON.parse(event.body);
await dynamodb.put({
TableName: 'Products',
Item: {
id: uuid(),
...product,
createdAt: new Date().toISOString(),
},
});
return {
statusCode: 201,
body: JSON.stringify(product),
};
};
GraphQL API
// Apollo Server Lambda
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateLambdaHandler } from '@as-integrations/aws-lambda';
const typeDefs = `
type Query {
users: [User!]!
user(id: ID!): User
}
type User {
id: ID!
name: String!
email: String!
}
`;
const resolvers = {
Query: {
users: async () => {
const result = await dynamodb.scan({ TableName: 'Users' });
return result.Items;
},
user: async (_, { id }) => {
const result = await dynamodb.get({
TableName: 'Users',
Key: { id },
});
return result.Item;
},
},
};
const server = new ApolloServer({ typeDefs, resolvers });
export const handler = startServerAndCreateLambdaHandler(server);
Database Options
DynamoDB (AWS)
NoSQL, serverless-native:
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand, GetCommand } from '@aws-sdk/lib-dynamodb';
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
// Write
await docClient.send(new PutCommand({
TableName: 'Users',
Item: { id: '123', name: 'Alice', email: 'alice@example.com' },
}));
// Read
const result = await docClient.send(new GetCommand({
TableName: 'Users',
Key: { id: '123' },
}));
Fauna
Globally distributed, serverless database:
import { Client, query as q } from 'faunadb';
const client = new Client({
secret: process.env.FAUNA_SECRET,
});
// Create
await client.query(
q.Create(q.Collection('users'), {
data: { name: 'Alice', email: 'alice@example.com' },
})
);
// Query
const users = await client.query(
q.Map(
q.Paginate(q.Documents(q.Collection('users'))),
q.Lambda('ref', q.Get(q.Var('ref')))
)
);
PlanetScale
Serverless MySQL:
import { connect } from '@planetscale/database';
const conn = connect({
url: process.env.DATABASE_URL,
});
const users = await conn.execute('SELECT * FROM users');
Supabase
PostgreSQL-based, serverless-friendly:
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_KEY
);
const { data, error } = await supabase
.from('users')
.select('*')
.eq('active', true);
Authentication
AWS Cognito
import { CognitoIdentityProviderClient, InitiateAuthCommand } from '@aws-sdk/client-cognito-identity-provider';
const client = new CognitoIdentityProviderClient({});
export const handler = async (event) => {
const { username, password } = JSON.parse(event.body);
const command = new InitiateAuthCommand({
AuthFlow: 'USER_PASSWORD_AUTH',
ClientId: process.env.COGNITO_CLIENT_ID,
AuthParameters: {
USERNAME: username,
PASSWORD: password,
},
});
const response = await client.send(command);
return {
statusCode: 200,
body: JSON.stringify({
token: response.AuthenticationResult.AccessToken,
}),
};
};
Auth0
import { ManagementClient } from 'auth0';
const auth0 = new ManagementClient({
domain: process.env.AUTH0_DOMAIN,
clientId: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
});
export const handler = async (event) => {
const token = event.headers.authorization?.replace('Bearer ', '');
// Verify token
const user = await auth0.getUser({ id: decodedToken.sub });
return {
statusCode: 200,
body: JSON.stringify(user),
};
};
Background Jobs
SQS + Lambda
// Producer
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
const sqs = new SQSClient({});
export const handler = async (event) => {
await sqs.send(new SendMessageCommand({
QueueUrl: process.env.QUEUE_URL,
MessageBody: JSON.stringify({
userId: event.userId,
action: 'send-email',
}),
}));
return { statusCode: 202 };
};
// Consumer
export const handler = async (event) => {
for (const record of event.Records) {
const message = JSON.parse(record.body);
if (message.action === 'send-email') {
await sendEmail(message.userId);
}
}
};
Upstash (Redis-based)
import { Queue } from '@upstash/qstash';
const queue = new Queue({
token: process.env.QSTASH_TOKEN,
});
// Enqueue
await queue.publish({
url: 'https://example.com/api/process',
body: JSON.stringify({ userId: '123' }),
});
// Process
export const handler = async (event) => {
const { userId } = JSON.parse(event.body);
await processUser(userId);
};
File Storage
S3
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({});
// Upload
export const upload = async (event) => {
const { filename, content } = JSON.parse(event.body);
await s3.send(new PutObjectCommand({
Bucket: process.env.BUCKET_NAME,
Key: filename,
Body: content,
}));
return { statusCode: 200 };
};
// Generate signed URL
export const getDownloadUrl = async (event) => {
const { filename } = event.queryStringParameters;
const command = new GetObjectCommand({
Bucket: process.env.BUCKET_NAME,
Key: filename,
});
const url = await getSignedUrl(s3, command, { expiresIn: 3600 });
return {
statusCode: 200,
body: JSON.stringify({ url }),
};
};
Cron Jobs
EventBridge Schedules
# serverless.yml
functions:
dailyReport:
handler: handler.generateReport
events:
- schedule: cron(0 9 * * ? *) # 9 AM UTC daily
export const generateReport = async () => {
const data = await fetchDailyData();
const report = generateReport(data);
await emailReport(report);
};
Performance Optimization
1. Cold Start Mitigation
// Keep connections warm
let dbConnection;
export const handler = async (event) => {
// Reuse connection if exists
if (!dbConnection) {
dbConnection = await createConnection();
}
const result = await dbConnection.query(event.query);
return result;
};
2. Provisioned Concurrency
# serverless.yml
functions:
api:
handler: handler.handler
provisionedConcurrency: 5 # Keep 5 instances warm
3. Connection Pooling
import { createPool } from 'mysql2/promise';
const pool = createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
connectionLimit: 1, # Serverless best practice
});
export const handler = async (event) => {
const [rows] = await pool.query('SELECT * FROM users');
return rows;
};
4. Caching
import { Redis } from '@upstash/redis';
const redis = new Redis({
url: process.env.UPSTASH_URL,
token: process.env.UPSTASH_TOKEN,
});
export const handler = async (event) => {
const cacheKey = `user:${event.userId}`;
// Check cache
let user = await redis.get(cacheKey);
if (!user) {
// Fetch from database
user = await db.getUser(event.userId);
// Cache for 1 hour
await redis.setex(cacheKey, 3600, JSON.stringify(user));
}
return user;
};
Cost Optimization
1. Right-Size Memory
# More memory = faster execution = potentially cheaper
functions:
api:
handler: handler.handler
memorySize: 1024 # Test different sizes
2. Batch Processing
// Process multiple items per invocation
export const handler = async (event) => {
const items = event.Records; // Up to 10,000 from SQS
await Promise.all(
items.map(item => processItem(item))
);
};
3. Set Timeouts
functions:
quickApi:
handler: handler.handler
timeout: 3 # Don't use default 6 seconds if not needed
Monitoring and Debugging
CloudWatch Logs
export const handler = async (event) => {
console.log('Event received:', JSON.stringify(event));
try {
const result = await processEvent(event);
console.log('Success:', result);
return result;
} catch (error) {
console.error('Error:', error);
throw error;
}
};
X-Ray Tracing
import AWSXRay from 'aws-xray-sdk-core';
import AWS from 'aws-sdk';
const dynamodb = AWSXRay.captureAWSClient(new AWS.DynamoDB.DocumentClient());
export const handler = async (event) => {
// Traced automatically
const result = await dynamodb.get({
TableName: 'Users',
Key: { id: event.userId },
});
return result.Item;
};
When to Use Serverless
Great for: β Variable/unpredictable traffic β Event-driven workloads β Rapid prototyping β Microservices β Background jobs β API backends
Consider alternatives for: β Long-running processes (> 15 minutes) β Consistent high traffic (cheaper to use servers) β Stateful applications β Real-time/WebSocket intensive β Complex deployments
Conclusion
Serverless architecture offers incredible benefits:
- No server management
- Automatic scaling
- Pay-per-use pricing
- Faster time to market
But it requires new thinking:
- Stateless design
- Event-driven patterns
- Distributed systems knowledge
- Cost awareness
Start small, learn the patterns, and scale as needed. Serverless isn't the answer to everything, but for the right workloads, it's transformative.
Ready to go serverless?
Jordan Patel
Web Developer & Technology Enthusiast