Why your components break in production (and how to fix it)
How to build design systems that gracefully handle the complexities of real-world API data
In my previous articles on APIs – Simplifying APIs: A designer's guide to better collaboration and Designing with APIs: A practical guide for beginners – I covered the fundamentals of understanding APIs, including endpoints and parameters, as well as how to design with them in mind using techniques like early collaboration and designing for variability.
In this article, we’ll focus on API constraints – something that can initially feel restrictive, but is often responsible for driving innovation. Let's explore how we can transform these technical boundaries into opportunities for creating design systems that scale gracefully across complex products.
Rethinking your design system for API realities
Traditional design systems often present components in their ideal state – perfectly formatted content, optimal data lengths, and predictable structures. But real-world API data is messy, variable, and sometimes completely unexpected.
One of the things I value most about consulting is having the opportunity to work both as an external advisor, and an embedded team contributor, depending on the client’s needs.
In a recent engagement, I encountered a painfully familiar pattern: the team had crafted visually stunning components for their design system, but hadn't properly stress-tested them against the chaos of real API data. When these components hit production, they'd break unpredictably, forcing developers to implement their own solutions that sidestepped the design system entirely.
This kicked off a frustrating cycle: Designs that looked (visually and conceptually) incredible during design reviews, but collapsed when actual data flowed through them. Developers, understandably frustrated, started building their own implementations. The design systems team scrambled to contain the growing inconsistencies, product managers grew increasingly impatient with mounting delays, and design reviews devolved from collaborative sessions into exercises in feature-stripping – all to reduce the implementation burden on an already stretched development team.
The path forward became clear: the team needed to stop treating API variability as an implementation hurdle, and start embracing it as a fundamental design constraint. Instead of fighting against unpredictable data, what if components were designed to thrive in this messiness from the ground up? This perspective shift – seeing constraints as creative catalysts rather than limitations – ultimately transformed how the team approached component design.
Creating data contracts for components
One of the most powerful tools in creating API-friendly design systems is the concept of "data contracts" – clear definitions of what information a component expects, in what format, and how it should behave when that expectation isn't met.
For each component in our system, we created a structured data contract that defined:
Required fields (component won't render without these)
Optional fields (component gracefully adapts if these are missing)
Field format expectations (numbers, strings, dates, etc.)
Fallback behaviours for missing or malformed data
Loading and error states for each component
This approach completely transformed how our design and development teams collaborated. Instead of endless back-and-forth when components broke, teams had a shared understanding of data requirements and boundaries.
💭 Example: For a transaction card component, the contract specified that the transaction amount and date were required, while the merchant logo was optional. If the logo was missing, the component would show the first letter of the merchant name in a branded circle. This clear contract meant that developers could confidently implement the component regardless of data completeness.
🔑 Practical takeaway: Start by auditing your most critical components and creating simple data contracts for each. Even a basic spreadsheet that lists required vs. optional fields can dramatically improve implementation consistency. Share these contracts with both design and development teams to create a common language around component data expectations.
Building a state matrix for API-driven components
Beyond individual data contracts, we developed a systematic approach to component states based on API realities:
Loading state: What the component shows while waiting for API data
Empty state: How the component appears when the API returns no results
Error state: What happens when the API request fails
Partial data state: How the component adapts when some data is missing
Overflow state: How the component behaves when there's too much data
For every component, we designed and documented each of these states, creating a matrix that developers could reference when implementing the system.
🔑 Practical takeaway: Create a template for your component documentation that requires designers to address each of these five states before a component can be considered "complete." For existing components, prioritise adding partial data states first, as these typically cause the most issues in production.
Turning rate limits into better experiences
Many APIs restrict the number of requests you can make in a given timeframe – a constraint that can initially feel limiting, but can actually drive more thoughtful design decisions. When faced with these technical boundaries, the key question becomes: How might these constraints lead to a better user experience?
Curating information rather than showing everything
Rather than displaying every possible statistic (which would require more API calls), we narrowed our focus to the most relevant data points. This didn't just help us stay within rate limits – it made the interface clearer and less overwhelming for users.
We categorised data into:
Essential (always fetch)
Important (fetch when possible)
Supplementary (fetch only on demand)
This tiered approach meant we could prioritise our API requests for the most critical information while deferring less important data until later or loading it only when requested.
🔑 Practical takeaway: For each data-heavy feature in your product, create a simple prioritisation exercise with stakeholders. Use a spreadsheet to list all potential data points and collaboratively categorise them into essential, important, and supplementary groups. This not only helps with API efficiency but often reveals that many "required" data points aren't actually critical to the user experience.
Implementing progressive enhancement with APIs
Building on this approach, we designed our components to support progressive enhancement based on API availability:
Components first render with essential data
Important data loads when rate limits allow
Supplementary data becomes available through user interaction
This pattern scales beautifully across design systems because it establishes a consistent mental model for how components evolve as more data becomes available – building on the concept of adaptability I discussed in my Designing with APIs article, where we explored how to design interfaces that adapt to changing data.
💭 Example: On our player profile cards, we always displayed name, team, and position (essential data). Performance statistics loaded subsequently (important data), while detailed career history was available through an expandable section (supplementary data). This approach preserved a consistent visual structure while working within API constraints.
The progressive enhancement approach also proved surprisingly beneficial for our users. In user testing, we discovered that gradually revealing information actually helped people absorb complex statistics more effectively than showing everything at once. What started as a technical workaround became a genuine UX improvement.
🔑 Practical takeaway: Create a simple enhancement pattern that can be applied consistently across components. Document this pattern with clear examples so that teams understand how to apply progressive enhancement to new features.
Making waiting time valuable
In data-heavy applications, API calls sometimes take time – especially when handling complex queries or fetching information from multiple sources. Rather than treating this waiting period as dead time, smart design systems can transform it into an opportunity to enhance the user experience.
Skeleton screens evolved
While working on a travel booking app, we moved beyond basic skeleton screens (which simply mimic the layout of content) to what I call "information scaffolding."
Instead of generic grey boxes, we displayed useful contextual information that didn't require API calls while the main content loaded:
Maps of destinations using cached data
Travel tips from a preloaded content library
Average price ranges based on historical data
This gave users something meaningful to engage with while waiting, reducing perceived loading time and increasing satisfaction.
To implement this consistently across our design system, we created skeleton component variants that contained useful preloaded content rather than empty placeholder shapes. These components were designed to seamlessly transition to their data-filled states once the API returned results.
I've genuinely never understood why more products don't do this. It's a bit like being at a restaurant – would you rather stare at an empty table while waiting for your food, or have something interesting to read or nibble on? The answer seems obvious, yet most digital products leave users staring at empty grey boxes.
🔑 Practical takeaway: For your most complex or data-heavy screens, identify what valuable content could be shown immediately while waiting for API data. Create a library of content fragments that can be displayed during loading states, and incorporate these into your skeleton component variants.
Designing with temporal states
I've found it useful to think about API-driven components as having "temporal states" – states that exist across time rather than just at a single moment:
Anticipatory state: What is shown before an API call is made
Loading state: What is displayed during the API call
Response state: What appears after receiving the API response
Refresh state: How the component behaves when data is updated
By designing deliberately for each of these temporal states, we create components that feel alive and responsive rather than static and brittle.
🔑 Practical takeaway: Audit a key user journey in your product and identify how components transition between these temporal states. Look for inconsistencies or jarring transitions that could be smoothed out with more thoughtful state management. Document your findings and create a consistent pattern for handling these transitions across your design system.
Turning errors into trust-building moments
API errors are inevitable, but they don't have to damage user trust. Your design system can transform these moments from frustrations into opportunities – if you handle them thoughtfully.
While working on a healthcare application, we developed what I call "confidence-preserving error patterns" – components designed to maintain user confidence even when things go wrong. This expands on the error handling approaches I touched on in my Simplifying APIs article, where I discussed how APIs respond with error codes and how to design fallback states.
Graded error responses
Instead of a single generic error state, we created a graded system of error responses:
Recoverable background errors: Issues that can be resolved behind the scenes without user involvement (automatically retry API calls, use cached data, etc.)
Partial content errors: When some but not all data is available (show what you have, explain what's missing)
Interactive recovery errors: Problems that require user action but offer clear choices (refresh, try alternative, etc.)
Hard blocking errors: Critical failures that prevent core functionality (clear explanation with specific next steps)
By incorporating this nuanced approach to errors across our design system, we created consistent patterns that users could learn and understand, rather than unpredictable error handling that differed across features. This builds directly on the "Create user-friendly and trustworthy error states" section from my previous article, taking those principles to a system-wide level.
This might seem like overkill, but I've seen firsthand how proper error handling can make or break user trust. In healthcare particularly, where data is sensitive and often critical, a thoughtfully handled error can be the difference between a user trusting your system or abandoning it entirely.
🔑 Practical takeaway: Create a simple error taxonomy for your product based on the four categories above. For each type, design a consistent pattern that can be applied across components. Focus particularly on partial content errors, as these are often handled inconsistently.
Preserving context during errors
One of the most important principles we established was context preservation during errors. When an API failed, our components were designed to maintain as much context as possible, rather than replacing everything with an error message.
💭 Example: In our patient record component, if medication data couldn't be retrieved, we would show all the successfully loaded sections (demographics, allergies, vitals) while displaying a specific error just in the medications section. This maintained context and allowed users to continue working with the available information.
This pattern was systematised across our component library, ensuring that errors were handled consistently and gracefully throughout the application.
🔑 Practical takeaway: Review your current error handling approach and identify opportunities to preserve context. Update your design system documentation to explicitly recommend containing errors to the specific component or data section that failed, rather than displaying full-page error states. This systematic approach to error handling elevates the basic principles I outlined in my Simplifying APIs article to an organisation-wide standard.
Designing with dynamic data flows
Not all API data arrives in a neat, complete package. Some APIs provide streaming updates, while others involve long-running processes. These dynamic data flows present unique challenges for design systems.
Creating calm interfaces with real-time data
While building a financial dashboard that displayed real-time cryptocurrency prices, we faced a challenge: constant price updates created a chaotic interface with numbers flickering continuously, making it difficult for users to focus.
We developed a set of patterns for handling real-time updates in a way that provided information without overwhelming users:
Visual dampening: Small changes triggered subtle visual indicators rather than immediate numeric updates
Update batching: Changes were collected and applied at regular intervals rather than immediately
Focus zones: Users could designate certain areas where they wanted to see real-time updates, while other areas remained stable
These patterns became part of our design system's guidance for real-time components, ensuring a consistent approach across the product.
I'm particularly proud of the focus zones concept. It came from watching how traders set up their multiple screens – they don't want everything flashing and changing constantly, but rather create focused areas of attention with different update cadences. We brought that same thinking to our single-screen experience.
🔑 Practical takeaway: If your product includes real-time data, create a simple pattern library for handling updates at different thresholds. Document when to use visual indicators versus numeric changes, and establish consistent animation patterns for different types of updates.
Designing for asynchronous processes
Some API operations trigger long-running processes that don't complete immediately. Traditional loading indicators don't work well for these scenarios since they might run for minutes or even hours.
For a document processing system, we developed specialised components for asynchronous operations:
Process tracker: Shows multiple stages of a long-running operation
Background task component: Allows users to continue working while monitoring progress
Resumable operation patterns: Handles cases where users might leave and return before a process completes
By incorporating these patterns into our design system, we ensured that all asynchronous operations had a consistent feel across the product.
🔑 Practical takeaway: Identify any long-running processes in your product and create a consistent pattern for tracking them. Ensure that these components provide appropriate feedback while allowing users to continue their work.
Component API maturity model
As our design system evolved, we developed a ‘Component API Maturity Model’ – a framework for evaluating how well components handle the realities of API data. This model has five levels:
Static: Component works with predictable, hardcoded data only
Adaptive: Component handles basic variations in data format and content
Resilient: Component gracefully manages missing or malformed data
Responsive: Component appropriately handles loading, error, and empty states
Dynamic: Component works seamlessly with real-time or streaming data
This gave us a framework to assess components and prioritise improvements, gradually elevating our entire system and its capability. We focused on progressive improvement, recognising that not every component needs to reach the highest level to deliver significant value.
🔑 Practical takeaway: Assess your most critical components against this maturity model. Identify which level each component currently achieves and create a roadmap for gradually improving their API resilience. Focus first on moving components from Static to Adaptive, as this provides the most immediate value.
Creating design systems that embrace API realities isn't just about better technical implementation – it fundamentally changes how users experience your product. By designing components that gracefully handle the messy, dynamic nature of real-world data, you create interfaces that feel more reliable, trustworthy, and polished.
Instead of seeing API constraints as limitations, use them as creative catalysts that push your design system to be more thoughtful and resilient. By establishing clear data contracts, designing for all possible states, and creating consistent patterns for temporal interactions, you'll build a system that scales beautifully across complex data-driven products. This represents the natural evolution of the collaborative mindset I advocated for in my earlier articles – moving from individual design decisions to systematic approaches.
The best design systems don't just look good in static mockups – they thrive in the dynamic, sometimes unpredictable world of API data. And that's where the real magic happens – when your components can handle whatever the API throws at them while maintaining a coherent, thoughtful user experience.
Thanks for reading! This article is also available on Medium, where I share more posts like this. If you're active there, feel free to follow me for updates.
I'd love to stay connected – join the conversation on X, or connect with me on LinkedIn to talk design, digital products, and everything in between.