Frontend Architecture

Frontend Architecture

The frontend of Madoc is built using ReactJS (opens in a new tab) with server side rendering. There are 2 distinct frontend applications: the public-facing site and the admin interface.


Madoc has been built from the ground up using TypeScript, with strong typing throughout the codebase and for all API calls. Types are shared between the frontend and Node backend. Where possible we try to use dependencies that contain their own type definitions.

Strong types ensure that the Madoc codebase is resilient to change and can be refactored easily. It also makes it easier to understand the codebase and to contribute to it with an IDE that supports TypeScript.

React with SSR

React was chosen for its component-based architecture and its ability to render on both the server and the client. It's also a very popular framework with a large community and a lot of support. Madoc uses react-router for routing and a react-i18next provider for internationalisation. Each page that is defined as a route defines which data it intends to load on the page. This data is then loaded on the server in parallel and passed to the client. This means that the page is rendered on the server with all the data it needs to render. This is known as server-side rendering (SSR).

An example of the server rendering for a page:

function ViewUser() {
  // This is a hook that will read the key and data defined below.
  // The data is stored on the React component itself, which enables the server to also
  // read the data and pass it to the client.
  const { data } = useData(ViewUser);
  return <div>...</div>
serverRendererFor(ViewUser, {
  // The key is used to identify the data that is loaded for this page.
  // It has access to the `params` from the route and also the `query` from the URL.
  getKey: params => ['global-get-user', { userId: Number(params.userId) }],
  // The data is loaded using the API client. The "vars" is the object returned
  // from the getKey function. This function is async and can return a promise.
  getData: (key, vars, api) => {
    return api.siteManager.getUserById(vars.userId);

React Query

For data fetching, Madoc uses React Query (opens in a new tab). This is a library that provides a declarative way to fetch data from the server and cache it. It also provides a way to invalidate the cache when data changes on the server. This is used for all data fetching in Madoc, including API calls. It also includes useful helpers for managing pagination and other common data fetching patterns.

Monolithic API Client

Currently most API calls are made using a monolithic API client. This is a single file that contains all the API calls for the frontend. This is not ideal, but it does mean that all API calls are in one place and can be easily refactored when the API changes. In the future we may split this up into separate API clients for each area of the application. Madoc currently has over 300 API endpoints, so this is a large task.

Plugins and extensions

For various parts of the frontend, we use a plugin system. This allows us to extend the functionality of the frontend without having to modify the core codebase. These can be enabled and disabled in the admin interface. Plugins can provide new routes, new components (page blocks) and new themes. Plugins are loaded on the server and the client. More information about plugins can be found in the Extension points section.


Madoc originally used a mixture of Styled Components (opens in a new tab) and CSS Modules for styling. This was not ideal as it meant that styling was not consistent across the codebase and was challenging to integrate Server Side Rendering without impacting performance. We are in the process of moving to using Tailwind CSS (opens in a new tab) which is a utility-first CSS framework that scales well and is easy to integrate with React.