
Serverless architecture has matured significantly, becoming a viable solution for applications of all sizes. As someone who's built backend services at Dentira Labs, I've seen firsthand how serverless can simplify infrastructure management while providing excellent scalability. In 2025, the serverless ecosystem offers more features, better tooling, and improved developer experience than ever before.
Serverless computing has evolved beyond just Lambda functions. The ecosystem now includes:
Design your application around API endpoints that map to serverless functions:
// api/users/index.ts
export async function handler(event: APIGatewayEvent) {
const { httpMethod, pathParameters } = event;
switch (httpMethod) {
case 'GET':
return await getUsers(pathParameters);
case 'POST':
return await createUser(JSON.parse(event.body || '{}'));
default:
return {
statusCode: 405,
body: JSON.stringify({ error: 'Method not allowed' }),
};
}
}
async function getUsers(params: any) {
// Fetch from database
const users = await db.query('SELECT * FROM users');
return {
statusCode: 200,
body: JSON.stringify(users),
};
}
Leverage event sources to trigger functions:
// Process file uploads
export const processFile = async (event: S3Event) => {
for (const record of event.Records) {
const bucket = record.s3.bucket.name;
const key = record.s3.object.key;
// Process file
const result = await processImage(bucket, key);
// Trigger another function
await invokeFunction('updateMetadata', { fileId: result.id });
}
};
Break your application into focused functions:
project/
├── functions/
│ ├── auth/
│ │ └── handler.ts
│ ├── users/
│ │ └── handler.ts
│ ├── orders/
│ │ └── handler.ts
│ └── notifications/
│ └── handler.ts
└── shared/
└── utils.ts
Vercel has become a popular choice for full-stack serverless applications:
// app/api/products/route.ts
export async function GET(request: Request) {
const products = await fetchProducts();
return Response.json(products);
}
export async function POST(request: Request) {
const data = await request.json();
const product = await createProduct(data);
return Response.json(product, { status: 201 });
}
Advantages:
Modern AWS development with TypeScript:
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
try {
const body = JSON.parse(event.body || '{}');
await docClient.send(
new PutCommand({
TableName: process.env.TABLE_NAME,
Item: {
id: crypto.randomUUID(),
...body,
createdAt: new Date().toISOString(),
},
})
);
return {
statusCode: 201,
body: JSON.stringify({ message: 'Item created' }),
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' }),
};
}
};
Run code at the edge for minimal latency:
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
// Route handling
if (url.pathname === '/api/users') {
return handleUsers(request);
}
return new Response('Not found', { status: 404 });
},
};
async function handleUsers(request: Request): Promise<Response> {
const users = await fetch('https://api.example.com/users');
const data = await users.json();
return Response.json(data, {
headers: {
'Cache-Control': 'public, max-age=3600',
},
});
}
Choose databases designed for serverless:
DynamoDB:
PlanetScale:
Supabase:
Serverless functions require efficient database connections:
// Use connection pooling
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 1, // Important for serverless
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
export async function query(text: string, params?: any[]) {
const start = Date.now();
const res = await pool.query(text, params);
const duration = Date.now() - start;
console.log('Executed query', { text, duration, rows: res.rowCount });
return res;
}
Implement caching to reduce database load:
import { Redis } from '@upstash/redis';
const redis = new Redis({
url: process.env.UPSTASH_REDIS_URL!,
token: process.env.UPSTASH_REDIS_TOKEN!,
});
export async function getCachedUser(userId: string) {
const cacheKey = `user:${userId}`;
// Try cache first
const cached = await redis.get(cacheKey);
if (cached) return cached;
// Fetch from database
const user = await db.getUser(userId);
// Cache for 5 minutes
await redis.setex(cacheKey, 300, JSON.stringify(user));
return user;
}
Reduce cold start times:
// ✅ Keep dependencies minimal
import { specificFunction } from 'library';
// ❌ Avoid large imports
import * as entireLibrary from 'library';
// ✅ Initialize outside handler
const db = initializeDatabase();
export const handler = async (event) => {
// Use pre-initialized connection
return await db.query(event.query);
};
Implement comprehensive error handling:
export const handler = async (event: any) => {
try {
// Your logic
return {
statusCode: 200,
body: JSON.stringify({ success: true }),
};
} catch (error) {
console.error('Error:', error);
// Return appropriate error response
return {
statusCode: error.statusCode || 500,
body: JSON.stringify({
error: error.message || 'Internal server error',
}),
};
}
};
Manage secrets securely:
// Use platform-specific secret management
const apiKey = process.env.API_KEY;
// For AWS, consider AWS Secrets Manager
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const secretsClient = new SecretsManagerClient({});
async function getSecret(secretName: string) {
const response = await secretsClient.send(
new GetSecretValueCommand({ SecretId: secretName })
);
return JSON.parse(response.SecretString || '{}');
}
Implement proper observability:
// Structured logging
import { Logger } from '@aws-lambda-powertools/logger';
const logger = new Logger();
export const handler = async (event: any) => {
logger.info('Processing request', {
requestId: event.requestContext?.requestId,
path: event.path,
});
try {
// Your logic
logger.info('Request processed successfully');
} catch (error) {
logger.error('Request failed', { error });
throw error;
}
};
Write testable code:
// Separate business logic from handler
export async function processOrder(orderData: OrderData) {
// Business logic
const order = await createOrder(orderData);
await sendConfirmation(order);
return order;
}
// Handler just orchestrates
export const handler = async (event: any) => {
try {
const order = await processOrder(JSON.parse(event.body));
return {
statusCode: 201,
body: JSON.stringify(order),
};
} catch (error) {
return {
statusCode: 400,
body: JSON.stringify({ error: error.message }),
};
}
};
// Easy to test
describe('processOrder', () => {
it('creates order successfully', async () => {
const result = await processOrder(mockOrderData);
expect(result.id).toBeDefined();
});
});
Choose appropriate memory and timeout:
# serverless.yml
functions:
api:
handler: index.handler
memorySize: 512 # Start small, increase if needed
timeout: 10 # Set reasonable timeout
reservedConcurrency: 10 # Control concurrency
Edge functions are often cheaper and faster:
// Edge function - runs close to users
export const config = {
runtime: 'edge',
};
export default async function handler(req: Request) {
// Fast response, lower cost
return new Response('Hello from the edge!');
}
Reduce function invocations with caching:
// Cache at API Gateway level
export const handler = async (event: any) => {
const cacheKey = `cache:${event.path}`;
const cached = await getFromCache(cacheKey);
if (cached) {
return {
statusCode: 200,
headers: { 'Cache-Control': 'max-age=3600' },
body: JSON.stringify(cached),
};
}
// Fetch and cache
const data = await fetchData();
await setCache(cacheKey, data, 3600);
return { statusCode: 200, body: JSON.stringify(data) };
};
Use tools like Serverless Framework or CDK:
# serverless.yml
service: my-service
provider:
name: aws
runtime: nodejs18.x
region: us-east-1
functions:
api:
handler: src/handler.api
events:
- http:
path: /api/{proxy+}
method: ANY
resources:
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: users
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
Here's a practical serverless architecture:
┌─────────────┐
│ Client │
└──────┬──────┘
│
▼
┌─────────────┐
│ API Gateway │
└──────┬──────┘
│
┌───┴───┐
▼ ▼
┌─────┐ ┌─────┐
│Auth │ │ API │
│Func │ │Func │
└──┬──┘ └──┬──┘
│ │
└───┬────┘
▼
┌─────────────┐
│ Database │
│ (Serverless)│
└─────────────┘
Looking ahead, we can expect:
Serverless architecture has become a mature, viable solution for building scalable applications. The key to success is understanding the platform you're using and designing your architecture accordingly.
At Dentira Labs, we've used serverless functions for various microservices, and the benefits in terms of operational simplicity and scalability have been significant. The key is to start simple, understand your platform's capabilities, and iterate based on real usage patterns.
Remember: serverless isn't just about functions—it's about building applications that scale automatically, cost-effectively, and reliably. Focus on your business logic, and let the platform handle the infrastructure.