DEV Community

Cover image for A Real-World Serverless Appointment Booking Backend on AWS
Bernard Chika Uwaezuoke
Bernard Chika Uwaezuoke

Posted on

A Real-World Serverless Appointment Booking Backend on AWS

From “Hello World” to a Real-World, Production-Ready System

Introduction: From Demo to Impact.

Most tutorials stop at “Hello from Serverless API!”
That is useful as a starting point, but it doesn’t solve a real problem.

In the real world, businesses do not need “Hello World.”
They need customers booked, schedules organized, and systems that scale without breaking.
In this blog, we’ll take a simple AWS Lambda + API Gateway setup and evolve it into a Smart Appointment Booking & Management API. A serverless backend that could realistically power a clinic, salon, or consulting business.

No servers.
No infrastructure headaches.
Just clean, scalable cloud architecture.

Problem Definition

Most small service-based businesses often face:
• Manual appointment booking
• Double bookings
• No automated reminders
• Poor visibility into schedules
Hiring engineers to manage servers is expensive.
This is where serverless architecture shines.

The Serverless Solution Architecture

Architectural Design

Prerequisites

  • Active AWS account
  • Basic understanding of serverless architecture
  • Knowledge of Python or Node.js
  • Familiarity with REST APIs and HTTP methods
  • Basic understanding of authentication (JWT)
  • Fundamental AWS IAM concepts
  • Ability to use API testing tools (Postman or cURL)

Now let's dive in!

Step 1: Creating the Core Lambda Function

1. Create the Lambda Function

  • Sign into AWS Console here

  • Search for and select Lambda in the global search bar.

Lambda search

  • Click on the Create a function button.

Create Function

  • Select Author from scratch

  • Configure:
    o Function Name: AppointmentAPI
    o Runtime: Python 3.x
    o Execution Role: Create new role with basic permissions

  • Click Create Function as shown in the images below.

lambda function

lambda function

2. Basic Lambda Handler (Foundation)

  • Go to the Code section of the function you just created and replace the body of code there with the code below.
import json

def lambda_handler(event, context):
    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "Appointment API is running"
        })
    }

Enter fullscreen mode Exit fullscreen mode
  • Click on the Deploy button to deploy the function.

Deploy function

  • Click on the Test button and Create an Event to test your function, then click the Test button as shown below.

Create Event

  • The function ran successfully!

Success

Step 2: Exposing the API with API Gateway

1. Create REST API

  • Search for and select API Gateway in the global search bar.

API Gateway

  • Click Create API

  • Choose REST API

  • Select Regional

  • Select Security Policy (as shown in the image below)

  • Click on Create API button

API Gateway

API Gateway2

2. Create Resource

  • Resource name: appointments

  • Path: /

API Resource

3. Create Methods

  • Inside the created API Resource, click on the Create method button.

Create method

Add the following methods:

Method Purpose
POST Create appointment
GET Fetch appointments
PUT Update appointment
DELETE Cancel appointment

Each method integrates with the same Lambda function.

Steps to Create a Method

For a POST /appointments method:

  • Select the /appointments resource

  • Click Actions → Create Method

  • Choose POST

  • Toggle the switch for Lambda proxy integration

  • Set Integration type to Lambda Function

  • Enable Lambda Proxy Integration

Choose your region and select your Lambda (AppointmentAPI)

Click Create method.

Post method

Post method 2

Post method 3
What it does: Creates a new appointment entry (writes to DynamoDB).

*For a GET /appointments method *

Repeat the same steps, but choose GET:

  • /appointments → Actions → Create Method → GET

  • Integration type: Lambda Function

  • Enable Lambda Proxy Integration

  • Select the same Lambda function

  • Click Create method

What it does: Returns appointments (for a user or provider).

For a PUT /appointments method.

Repeat again for PUT:

  • /appointments → Actions → Create Method → PUT

  • Integrate with the same Lambda

  • Enable Lambda Proxy Integration

  • Click Create method

What it does: Updates an appointment (reschedule, status update, etc.)

For a DELETE /appointments method.

Repeat for DELETE:

  • /appointments → Actions → Create Method → DELETE

  • Integrate with same Lambda

  • Enable Lambda Proxy Integration

  • Click Create method

What it does: Cancels an appointment (soft delete recommended, by setting status = canceled).

The four CRUD Methods.

CRUD Methods

Step 3: Designing the Data Model (DynamoDB)

Before we write a single line of booking logic, we need to get the data model right. In serverless systems, your database design is everything, because your API performance, cost, and scalability depend on it.

For this Smart Appointment Booking API, we’ll use Amazon DynamoDB: a fully managed NoSQL database designed for fast reads/writes at massive scale.

Setting up the DynamoDB Table

1. Open DynamoDB

  • Search for and select Lambda in the global search bar.

  • Click DynamoDB → Tables → Create table

2. Create the Appointments Table

  • On Create table pane, enter:

Table details

Table name: Appointments

Partition key (PK): userId (Type: String)

Sort key (SK): appointmentId (Type: String)

Table details

3. Choose Table Settings

  • Under Table settings, choose On-demand (Pay per request)

This is best for MVPs and unpredictable traffic

  • Click Create table button.

4. Add a Global Secondary Index (GSI) for Fast Lookups

This is optional, but it’s what makes your API feel “real” when you need queries like:

“Show all appointments on a date”

“Show all booked appointments”

“Show all appointments by service type”

Create a GSI for date

  • Open the Appointments table

  • Go to Indexes tab

  • Click Create index

Index details

Partition key: date (String)

Sort key (optional): time (String)

Index name: date-time-index

  • Leave other configurations as default

  • Click Create index

Index

Now you can query: “All appointments on 2026-02-20, sorted by time.”

5. Give Lambda Permission to Use DynamoDB

Lambda won’t be able to read/write until IAM allows it.

  • Go to Lambda → our function (AppointmentAPI)

  • Open ConfigurationPermissions

  • Click the Execution role (opens IAM)

  • Click Add permissions → Attach policies

Attach one of these:

Quick (broad, good for learning) but goes against the Least Privilege Principle:

AmazonDynamoDBFullAccess

Better (recommended for real projects):

Create a custom policy allowing only:

dynamodb:PutItem

dynamodb:GetItem

dynamodb:UpdateItem

dynamodb:DeleteItem

dynamodb:Query

dynamodb:Scan (optional)

  • However, for purpose of this demo, we will use the first option (AmazonDynamoDBFullAccess).

Lambda Permission (IAM Role)
Lambda Role
Attach Policy (AmazonDynamoDBFullAccess)
IAM Permission

  • Click on Add permission button.

Step 4: Booking an Appointment (Business Logic)

Now that DynamoDB is ready and API Gateway routes requests to Lambda, we are ready to build the real booking engine.

Here is what we will implement:

  • POST /appointments to creates a new booking

  • Validates required fields

  • Generates a unique appointmentId

  • Writes the appointment into DynamoDB

  • Returns a clean JSON response

1. Lambda: Read the Incoming Request

When API Gateway triggers Lambda (proxy mode), the body arrives as a string under event["body"].

  • What Lambda Receives
  • event["httpMethod"] → "POST"
  • event["path"] → "/appointments"
  • event["body"] → JSON string

2. Implement Appointment Booking Logic (Python)

Create appointment + store in DynamoDB

Since our DynamoDB table is named Appointments and has keys:
userId (PK), appointmentId (SK)

  • Go to the Lambda function and add the body of codes below:
import json
import os
import uuid
import boto3
from datetime import datetime

dynamodb = boto3.resource("dynamodb")
TABLE_NAME = os.environ.get("APPOINTMENTS_TABLE", "Appointments")
table = dynamodb.Table(TABLE_NAME)

def lambda_handler(event, context):
    method = event.get("httpMethod")
    path = event.get("path")

    # Route: POST /appointments
    if method == "POST" and path == "/appointments":
        return handle_create_appointment(event)

    return response(404, {"error": "Route not found"})

def handle_create_appointment(event):
    # In a real system, userId comes from Cognito JWT claims.
    # For now, we’ll hardcode a sample user.
    user_id = "user_123"

    # Parse JSON body
    try:
        body = json.loads(event.get("body") or "{}")
    except json.JSONDecodeError:
        return response(400, {"error": "Invalid JSON body"})

    service = body.get("service")
    date = body.get("date")
    time = body.get("time")

    # Validate required fields
    missing = [k for k in ["service", "date", "time"] if not body.get(k)]
    if missing:
        return response(400, {"error": f"Missing fields: {', '.join(missing)}"})

    # Basic input validation (light but helpful)
    if not is_valid_date(date):
        return response(400, {"error": "Invalid date format. Use YYYY-MM-DD"})
    if not is_valid_time(time):
        return response(400, {"error": "Invalid time format. Use HH:MM (24h)"})

    appointment_id = f"apt_{uuid.uuid4().hex[:10]}"
    item = {
        "userId": user_id,
        "appointmentId": appointment_id,
        "service": service,
        "date": date,
        "time": time,
        "status": "BOOKED",
        "createdAt": datetime.utcnow().isoformat() + "Z"
    }

    # Write to DynamoDB
    table.put_item(Item=item)

    # Return response
    return response(201, {
        "appointmentId": appointment_id,
        "status": "BOOKED"
    })

def response(status_code, body):
    return {
        "statusCode": status_code,
        "headers": {
            "Content-Type": "application/json"
        },
        "body": json.dumps(body)
    }

def is_valid_date(date_str):
    try:
        datetime.strptime(date_str, "%Y-%m-%d")
        return True
    except Exception:
        return False

def is_valid_time(time_str):
    try:
        datetime.strptime(time_str, "%H:%M")
        return True
    except Exception:
        return False

Enter fullscreen mode Exit fullscreen mode
  • Click on Deploy

3. Configure Lambda Environment Variable

Instead of hardcoding table name.

  • Navigate to Lambdaour function (AppointmentAPI) → Configuration

On the left pane, click on Environment variables → Edit and Add environment variable:

Key: APPOINTMENTS_TABLE

Value: Appointments

  • Click Save

4. Create Stage in API Gateway

  • Go to API Gateway, click on our API (AppointmentRestAPI), on the left pane, under API: AppointmentRestAPI
    click on Stages, click on Create stage. Enter the following details:
    Stage name: prod
    Deployment: select date and time from the draw-down.

  • Leave others as default and click the Create stage button.

Stage

  • Then click on deploy using the created stage (prod)

Lambda Triggers API Routes

  • Go to our Lambda function, click on API Gateway under the AppointmentAPI and click on configuration. To see the triggers (The four routes of POST, GET, PUT and DELETE)

5. Test the Endpoint

Using cURL
Replace the URL with your API Gateway Invoke URL:

(In Windows PowerShell)

Invoke-RestMethod `
  -Method POST `
  -Uri "https://8cjba7n9wb.execute-api.us-east-1.amazonaws.com/prod/appointments" `
  -ContentType "application/json" `
  -Body '{"service":"haircut","date":"2026-02-20","time":"14:00"}'

Enter fullscreen mode Exit fullscreen mode

Response in PowerShell

Response

Table item in DynamoDB table
Go to DynamoDB, click on our table and find the new item posted there by the Lambda function, triggered by the API.

At this point, we’ve built a real booking engine.

Step 5: Set Notifications & Reminders (SNS + SES)

Bookings are pointless if people forget them. Real systems reduce no-shows with instant confirmations and scheduled reminders.

So, let's add:

  • SNS for SMS alerts
  • SES for email confirmations

Triggers for:

  • Booking confirmation (immediate)
  • 24-hour reminder (scheduled)
  • Cancellation alert (immediate)

SMS Notifications with Amazon SNS

1. Create (or choose) an SNS setup for SMS

  • Go to Amazon SNS in AWS Console
  • In the left menu, click Text messaging (SMS) (if shown)

  • Scroll down to SMS preferences (or click Edit SMS preferences if shown) as shown in the images below.

Set:

Default message type → Transactional

Monthly spending limit → set a small amount (e.g. 1 or 5 USD)

Click Save changes

SNS Setting

SMS setting

In many regions you can send SMS directly without creating a topic (you publish to a phone number).

2. Test SMS directly from the SNS console (optional but recommended)

  • On the same screen, click Publish text message (top right)

Enter:

  • Phone number → your phone number (with country code, e.g. +1...)
  • Message → Test SMS from Appointment API
  • Click Publish message

Publish sms

varify
If you receive the SMS, SNS is working correctly.

3. Allow your Lambda to send SMS

  • Go to AWS Lambda
  • Open your function (e.g. AppointmentAPI)
  • Go to Configuration → Permissions
  • Click the Execution role (opens IAM)

Attach this policy:

AmazonSNSFullAccess

  • Scroll down and click Add permissions button.

Lambda can now send SMS.

4. Send SMS from Lambda (this is all you need)
Add the body of code below to our Lambda code:

import boto3

sns = boto3.client("sns")

def send_sms(phone_number, message):
    sns.publish(
        PhoneNumber=phone_number,
        Message=message
    )

Enter fullscreen mode Exit fullscreen mode

Response (Booking confirmation)

send_sms(
    "+15551234567",
    "Booking confirmed: Haircut on 2026-02-20 at 14:00"
)
Enter fullscreen mode Exit fullscreen mode

5. When SMS is triggered

  • Booking confirmation

After successful POST /appointments

  • Cancellation alert

After DELETE /appointments or status update to canceled

  • 24-hour reminder

Sent from a scheduled Lambda (step no includede here)

Epilogue: Completing the Journey to a Full SaaS Backend

What we’ve built throughout this blog is a solid foundation, a functional, real-world serverless backend capable of handling bookings, persistence, and user-facing workflows. But modern SaaS systems don’t stop at functionality alone. They mature through security, reliability, deployment discipline, and cost awareness.

To evolve this solution into a fully production-grade SaaS backend, the following layers naturally complement what we have already covered:

Securing the API with Authentication

Adding authentication ensures that every request is trusted and every action is tied to an authenticated identity. This is where user isolation, role-based access, and data protection come into play, which is critical for any real application handling customer data.

Monitoring & Reliability

Visibility is what turns a working system into a reliable one. By introducing monitoring, logs, and alerts, the system becomes observable, debuggable, and resilient under real traffic and failure scenarios.

Deployment Strategy

A proper deployment strategy, separating development, staging, and production environments, that allows changes to be released safely and confidently. This transforms the project from a single deployment into a system that can evolve continuously.

Cost Breakdown: Why Serverless Wins

Understanding the cost model completes the architectural picture. With serverless, you pay only for usage, scale automatically with demand, and eliminate idle infrastructure—making this approach especially powerful for startups and growing businesses.

The Final Result

When these layers are added on top of the system we have built, the result is a complete SaaS-ready backend:

  • Secure
  • Scalable
  • Production-ready
  • Business-focused

This architecture is not theoretical. It is directly applicable to real industries and real customers.

This system could power:

  • Clinics managing patient appointments
  • Salons handling bookings and reminders
  • Consultants scheduling client sessions
  • Repair services coordinating visits and follow-ups

Closing Thought

The true value of serverless architecture isn’t just removing servers, it is in enabling teams to focus on business logic, user experience, and growth instead of infrastructure. What we have built here is not an endpoint, but a platform, one that can be extended, secured, observed, and scaled into a real product.

That is the difference between a tutorial and a real SaaS system.

Top comments (0)