←Back to Blog
β€’9 min readβ€’Infrastructure

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