Designing front-end APIs

Photo by Xiong Yan on Unsplash

Designing front-end APIs

API design is both science and art. The key principles of API design are often simple but for frontend engineers it gets a bit complex because we rely on Javascript which is very notorious for not supporting static typing. I generally work with Typescript to overcome this problem.

In most cases the choice of frameworks would decide how you go about deigning the API.

The principles of API design in frontend are :

  1. Strong typing and reusability

Most of the bugs in frontend code can be traced to type related issues. That is why a lot of large companies tend to use Typescript and I highly recommend use of Typescript. Typescript supports Types and Interfaces which vastly simplify API design and help us enforce it using static analysis tools and compiler.

I like to design frontends by first defining the smallest building blocks with very well defined types.

For example for a music app, we might have to often display a "Track". So I would like to define a Track as a type which is reusable across the app.

Example


type Track = {
  title: string;
  artist: Artist;
  album: Album;
  duration: number; // Duration in seconds
  genre: Genre; 
  releaseYear: number;
  isExplicit: boolean;
  trackNumber: number; // Position on an album
  streamingUrl?: string; // Optional URL for streaming
}

Note that how these objects look on client side often mirrors how they are defined in the backend. Using similar name for properties etc. simplifies conversion from backend responses to client side objects.

Enforcing static types makes these objects reusable across the system.

  1. Separation of Concern

The second principle I follow is what I call "Separation of Concerns". Once you have defined your data model, you think about how your application can be broken into smaller components and services.

For example communication with the server is one such aspect. Whether we are using GraphQL or REST Apis or something else, it makes sense to break that functionality into smaller Services with different implementations. For example

interface User {
  // ... other User properties (e.g., id, name, email, etc.)
}

interface UserLibraryResponse {
  tracks: Track[]; // Assuming you have the Track type defined
  currentPage: number;
  totalPages: number;
}

interface UserService {
  getUserLibrary(page: number, limit: number): Promise<UserLibraryResponse>;
}

Here user service is responsible for executing all actions related to the user. How it communicates with the server etc. can then be implemented separately.

This is the "separation of concern" principle that makes sure we do not stuff too many action items into different components.

  1. Bottom up approach for UI component API

Once we have over data model properly design and key actions in the app defined in services we can think more deeply about UI components.

In React one of the hardest problems is that of state management across components. Moving state up requires complex bubbling logic and moving state down requires prop drilling. This is complex and can be simplified using state management libraries like Redux.

I like to think of entire application as a composition of UI components and each component can be then seen as composition of smaller components.

I like to develop a library of smaller possible components with well defined API interface and then define complex components as part of that.

To give you an example.

A Button and IconButton. IconButton can be seen as a special type of button.

import React from 'react';
import './Button.css'; // Or your preferred styling method

// Base Button Component
interface ButtonProps {
  children: React.ReactNode;
  onClick?: () => void;
  type?: 'button' | 'submit' | 'reset';  // Default to 'button'
  variant?: 'primary' | 'secondary' | 'outline'; // Example variants
  disabled?: boolean;
}

const Button: React.FC<ButtonProps> = ({ children, onClick, type = 'button', variant, disabled }) => {
  // Determine the CSS class based on props (e.g., button button-primary)
  const buttonClass = // ... your styling logic here

  return (
    <button 
      className={buttonClass} 
      onClick={onClick} 
      type={type} 
      disabled={disabled}
    >
      {children}
    </button>
  );
};


// IconButton Component
interface IconButtonProps extends ButtonProps {
  icon: React.ReactNode; // Icon to display
}

const IconButton: React.FC<IconButtonProps> = ({ icon, children, ...rest }) => {
  return (
    <Button {...rest}>
      {icon} 
      {children} {/* Text content within the button is optional */}
    </Button>
  );
};

export { Button, IconButton };

When designing an individual component, the API principle is to always use well defined Props that make sense for that component. For example in the above code we see that IconButton has a property Icon and rest of the properties are inherited from the general Button component.

Conclusion

Designing front end APIs is a complex work and there is no perfect way to do it. You should follow other resources which give in depth overview of front end technologies to master this art of front end api design.