The API Performance Renaissance: When AI Discovers the Power of Selective Data Loading
July 23-25, 2025 - Part 11
The Performance Realization
After implementing comprehensive search functionality and polishing the user interface, our Phoenix LiveView blog was feature-complete and visually stunning. But as I started using the API endpoints for content management, a performance issue became glaringly obvious:
The problem: Every API request for the posts list was returning full blog post content, including thousands of words of markdown text that API clients didn’t need for listing operations.
The symptoms:
- API responses were 5.8KB for simple post listings
- Mobile API clients were downloading unnecessary content
- Search and filtering operations were slower than they should be
The challenge: Implement a “summary-first” API architecture that returns lightweight post metadata by default, with full content available on demand.
What followed was Claude’s most elegant API design work yet—a comprehensive overhaul that reduced payload sizes by 39% while maintaining complete backward compatibility.
The API Performance Audit
Me: “The API is returning too much data for the posts list endpoint. We need to make it more efficient.”
Claude: “I’ll analyze the current API response structure and implement a summary-first approach with optional content inclusion…”
The Current API Bloat Analysis
Looking at a typical API response revealed the scope of the problem:
{
"posts": [
{
"id": 1,
"title": "Building a Blog with Claude 4",
"slug": "building-a-blog-with-claude-4",
"content": "# Building a Blog with Claude 4\n\nIt started with a question that's been nagging at me for months: **Can AI really build a complete, production-ready web application?** Not just generate some boilerplate code or fix a few bugs, but architect, implement, and deploy an entire system with modern best practices.\n\nI decided to find out.\n\nArmed with skepticism and a detailed 190-line XML specification, I threw down the gauntlet to Claude 4...", // 3000+ more characters
"subtitle": "An AI Development Adventure",
"excerpt": "Exploring whether AI can build production-ready applications from scratch...",
"tags": "elixir,phoenix,ai,development",
"published": true,
"published_at": "2025-06-28T10:00:00Z",
"inserted_at": "2025-06-28T09:45:00Z",
"updated_at": "2025-06-28T10:15:00Z"
}
// ... more posts with full content
]
}
The inefficiency: For a simple post listing, clients were downloading:
- Full markdown content (2-4KB per post)
- Metadata fields that weren’t always needed
- Internal timestamps that were rarely used
- Null values taking up space
The target: Reduce this to a lightweight summary format by default.
The Summary-First Architecture Design
Claude’s approach was architecturally elegant: Transform the API from content-first to summary-first with opt-in content loading.
The Flexible Parameter System
# Enhanced list_posts function with options
def list_posts(opts \\ []) do
page = Keyword.get(opts, :page, 1)
per_page = Keyword.get(opts, :per_page, 10)
tags = Keyword.get(opts, :tags, [])
search = Keyword.get(opts, :search)
series = Keyword.get(opts, :series, [])
allow_unpublished = Keyword.get(opts, :allow_unpublished, false)
include_content = Keyword.get(opts, :include_content, false) # NEW!
# Build query...
result
|> maybe_add_rendered_content(include_content, tags, search, page, per_page)
|> maybe_filter_fields(opts)
end
The key insight: Use the existing list_posts
function but add intelligent field filtering based on client needs.
The Intelligent Field Filtering System
defp maybe_filter_fields(posts, opts) do
exclude_fields = get_excluded_fields(opts)
Enum.map(posts, fn post ->
exclude_fields_from_struct(post, exclude_fields)
end)
end
defp get_excluded_fields(opts) do
excluded = [:__meta__, :rendered_content, :images]
# Always exclude internal fields
excluded = excluded ++ [:excerpt, :inserted_at, :published]
# Conditionally exclude content unless explicitly requested
excluded =
if Keyword.get(opts, :include_content, false), do: excluded, else: [:content | excluded]
# Future extensibility designed in
# excluded = if Keyword.get(opts, :include_images, true), do: excluded, else: [:images | excluded]
# excluded = if Keyword.get(opts, :include_metadata, true), do: excluded, else: [:subtitle, :excerpt | excluded]
excluded
end
defp exclude_fields_from_struct(%Post{} = post, fields) do
post
|> Map.from_struct()
|> Map.drop(fields)
|> remove_null_values() # Additional optimization
end
defp remove_null_values(map) do
map
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|> Map.new()
end
The sophistication:
- Systematic field exclusion based on use case
- Automatic null value removal for cleaner responses
- Extensible architecture for future filtering options
The Controller Integration
The API controller changes were minimal but powerful:
defmodule BlogWeb.API.PostController do
def index(conn, params) do
include_content = parse_boolean(params["include_content"])
opts = [
page: parse_integer(params["page"], 1),
per_page: parse_integer(params["per_page"], 10),
tags: parse_tags(params["tags"]),
search: params["search"],
include_content: include_content # NEW!
]
posts = Content.list_posts(opts)
render(conn, :index, posts: posts)
end
defp parse_boolean(nil), do: false
defp parse_boolean("true"), do: true
defp parse_boolean("false"), do: false
defp parse_boolean(_), do: false
end
The API Usage:
# Fast summary (default behavior)
GET /api/posts
# Response: 3600 bytes
# Full content when needed
GET /api/posts?include_content=true
# Response: 5800 bytes
The performance improvement: 39% reduction in default payload size.
The Dynamic JSON Rendering Challenge
The trickiest part was updating the JSON renderer to handle both Post structs and filtered maps dynamically:
The Original Rigid Approach
# PostJSON only worked with Post structs
def data(%Post{} = post) do
%{
id: post.id,
title: post.title,
slug: post.slug,
content: post.content, # Always included
# ... more hardcoded fields
}
end
Claude’s Dynamic Rendering Solution
def data(post) when is_struct(post) do
# Handle Post structs (backward compatibility)
post
|> Map.from_struct()
|> filter_fields()
end
def data(post) when is_map(post) do
# Handle filtered maps (new summary format)
filter_fields(post)
end
defp filter_fields(post_data) do
# Only include fields that exist and aren't internal
allowed_fields = [:id, :title, :slug, :content, :subtitle, :tags, :published_at, :series_id]
post_data
|> Map.take(allowed_fields)
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|> Map.new()
end
The elegance: Single JSON renderer handles both filtered maps and full structs seamlessly.
The Security and Flexibility Enhancement
Along with performance optimization, Claude enhanced API security and flexibility:
The allow_unpublished
Option
def get_post(id, opts \\ []) do
allow_unpublished = Keyword.get(opts, :allow_unpublished, false)
case RepoService.get(Post, id) do
{:ok, post} when allow_unpublished == true -> post
{:ok, post} when post.published == true and not is_nil(post.published_at) -> post
_ -> nil
end
end
The security model:
- Public endpoints: Only published posts (secure by default)
- Authenticated endpoints: Can access unpublished drafts with explicit option
-
Admin functions: Full access with
allow_unpublished: true
The Data Migration for Consistency
# Migration to ensure boolean consistency
defmodule Blog.Repo.Migrations.SetPublishedDefaultFalse do
use Ecto.Migration
def change do
# Ensure all posts have proper boolean values
execute("UPDATE posts SET published = 0 WHERE published IS NULL")
alter table(:posts) do
modify :published, :boolean, default: false, null: false
end
end
end
The reliability improvement: Eliminates potential null/boolean confusion in published status.
The Testing Comprehensive Coverage
Claude added comprehensive tests for all the new functionality:
describe "list_posts/1 with field filtering" do
test "excludes content by default" do
posts = Content.list_posts()
assert length(posts) > 0
refute Map.has_key?(List.first(posts), :content)
refute Map.has_key?(List.first(posts), :excerpt)
refute Map.has_key?(List.first(posts), :inserted_at)
end
test "includes content when requested" do
posts = Content.list_posts(include_content: true)
assert length(posts) > 0
assert Map.has_key?(List.first(posts), :content)
end
test "filters out null values" do
posts = Content.list_posts()
first_post = List.first(posts)
assert Enum.all?(first_post, fn {_key, value} -> !is_nil(value) end)
end
end
The coverage: Tests verify both performance optimization and backward compatibility.
The Real-World Performance Impact
After deployment, the performance improvements were immediately measurable:
API Response Size Comparison
Before optimization:
// GET /api/posts (5800 bytes)
{
"posts": [
{
"id": 1,
"title": "Building a Blog",
"content": "[3000 characters of markdown]",
"excerpt": "Some excerpt...",
"inserted_at": "2025-06-28T09:45:00Z",
"published": true,
"subtitle": "An Adventure"
// ... 8 more fields
}
]
}
After optimization:
// GET /api/posts (3600 bytes - 39% smaller)
{
"posts": [
{
"id": 1,
"title": "Building a Blog",
"slug": "building-a-blog",
"tags": "elixir,phoenix,ai",
"published_at": "2025-06-28T10:00:00Z"
// Only essential fields, no nulls
}
]
}
Mobile Performance Benefits
Before: Mobile clients downloading 23KB for a 4-post listing page. After: Mobile clients downloading 14KB for the same data.
The user experience improvement: Faster page loads, reduced data usage, improved responsiveness on slower connections.
The Extensible Architecture Foundation
The most impressive aspect of Claude’s implementation was building extensibility into the system:
Future-Ready Field Filtering
# Designed for easy extension
excluded = if Keyword.get(opts, :include_images, true), do: excluded, else: [:images | excluded]
excluded = if Keyword.get(opts, :include_metadata, true), do: excluded, else: [:subtitle, :excerpt | excluded]
excluded = if Keyword.get(opts, :include_timestamps, false), do: excluded, else: [:inserted_at, :updated_at | excluded]
Future API possibilities:
-
?include_images=false
- Exclude image references -
?include_metadata=false
- Minimal data for high-performance use cases -
?include_timestamps=true
- Full audit trail for admin interfaces
The Maintainable Filtering Pattern
defp get_excluded_fields(opts) do
[]
|> maybe_exclude_content(opts)
|> maybe_exclude_metadata(opts)
|> maybe_exclude_images(opts)
|> maybe_exclude_timestamps(opts)
|> add_always_excluded_fields()
end
The maintainability: Each filtering concern is separated into focused, testable functions.
The Backward Compatibility Achievement
Despite the significant architectural changes, the API maintained perfect backward compatibility:
Existing API clients: Continued working without any changes (they just got faster responses).
New API clients: Could opt into full content with ?include_content=true
.
Internal LiveView code: No changes required—still receives full Post structs.
The migration strategy: Zero-downtime deployment with immediate performance benefits.
What This Teaches About AI-Driven API Design
This API optimization project revealed several insights about AI-assisted performance engineering:
Where Claude Excelled
Systematic optimization: Claude identified all the performance bottlenecks and addressed them comprehensively.
Architectural thinking: The summary-first approach was an elegant solution that solved multiple problems simultaneously.
Extensibility planning: The field filtering system was designed for future enhancement, not just immediate needs.
Testing thoroughness: Complete test coverage ensured performance optimizations didn’t break functionality.
The Performance-First Mindset
Human insight: “This API is too slow” - identifying the user experience problem.
AI systematic solution: Claude analyzed the entire data flow, from database queries through JSON serialization, finding optimization opportunities at every layer.
The compound improvement: 39% payload reduction + null value filtering + extensible architecture = significantly better API performance with future scalability.
The Balance of Features and Performance
The challenge: Maintain rich functionality while optimizing for performance.
Claude’s approach: Don’t remove features—make them opt-in. Default to fast, allow full functionality when needed.
The result: Both API performance and API flexibility improved simultaneously.
The Recursive Content Optimization
As I write this post about API optimization, I’m using the very optimized API endpoints described in these words. The summary-first architecture that reduced payload sizes by 39% is now serving this content about itself.
The meta-achievement: I’m documenting API performance improvements using the faster API that was improved.
Even the documentation benefits from the performance optimizations it describes.
The Foundation for Future Scale
The summary-first API architecture creates a foundation for handling much larger scale:
Scalability Benefits
Reduced bandwidth: 39% fewer bytes per request means 39% more concurrent users on the same infrastructure.
Faster response times: Smaller payloads mean faster network transfers and JSON parsing.
Mobile optimization: Reduced data usage improves performance on slower mobile connections.
Caching efficiency: Smaller responses are more cache-friendly and improve CDN performance.
The Compound Infrastructure Effect
This performance optimization builds on previous architectural decisions:
- Distributed database provides global performance
- Observability monitoring measures API performance improvements
- Professional infrastructure scales efficiently with optimized payloads
The pattern: Each infrastructure improvement multiplies the benefits of other improvements.
Looking Forward: Performance as a Feature
The API performance renaissance transformed how we think about data loading:
Before: Return everything, let clients figure out what they need.
After: Return exactly what clients need, with full data available on demand.
This optimization created a template for future performance work: systematic analysis → architectural elegance → comprehensive testing → measurable improvements.
The next performance frontier: Cache-aware API responses and intelligent prefetching based on user behavior patterns captured by our observability infrastructure.
This post documents the API performance optimization that reduced default payload sizes by 39% while maintaining complete backward compatibility. The summary-first architecture described here now serves this content about itself through the optimized endpoints.
Sometimes the best performance improvements are the ones that make everything else faster too.