Rick and Morty

A data-rich explorer for navigating characters, locations, and episodes through the connected Rick and Morty universe.

Next.js
TypeScript
Axios
Sass
The Rick and Morty API
Rick and Morty project logo

Overview

Rick and Morty Explorer is a multi-page application for browsing the show's characters, locations, and episodes. Instead of presenting the API as three isolated catalogues, the interface connects its entities: a character leads to their origin, current location, first appearance, and complete episode history; locations and episodes lead back to their related characters.

The project was created as a practical study of the Next.js 13 App Router. Its primary goal was to explore Server Components, dynamic and static rendering strategies, URL-driven state, data fetching, caching behaviour, and page-level SEO in a real application rather than an isolated demo.

Product structure

The application is organised around three primary sections:

  • Characters - a searchable catalogue with status and gender filters.
  • Locations - paginated location pages with links to every known resident.
  • Episodes - an episode catalogue that reveals the full participating cast.

The home page provides a changing entry point by selecting random characters and showing the current totals for all three entity types. A persistent header, contextual back navigation, and cross-links between related records make it possible to move through the data without returning to a central search page.

Choosing a rendering strategy

Not every route has the same data requirements, so the application does not apply one rendering mode everywhere.

Character, location, and episode detail routes generate their valid parameters from the API. This allows Next.js to prepare entity pages ahead of time and gives every record a stable, indexable URL.

app/(characters)/character/[id]/page.tsx
export const generateStaticParams = async () => {
  const charactersCount = (await fetchCharacters()).data.info.count;
 
  return Array.from(
    { length: charactersCount },
    (_, index: number) => index + 1
  ).map((id) => ({
    id: id.toString()
  }));
};

The characters catalogue takes the opposite approach. Its result depends on search parameters supplied by the user, so the route is rendered dynamically. The home page is also dynamic because it deliberately chooses a new set of random characters for each request.

This split turned the project into a useful comparison between predictable entity pages and request-dependent discovery pages, while keeping the routing model consistent.

URL-driven filtering

Search, status, gender, and pagination are stored in the URL instead of hidden component state. Each active value is converted into an API parameter, while empty filters are omitted.

app/(characters)/characters/page.tsx
const currentPage = +(searchParams?.page ?? 1);
 
const filters = {
  ...(searchParams?.status && { status: searchParams.status }),
  ...(searchParams?.gender && { gender: searchParams.gender }),
  ...(searchParams?.name && { name: searchParams.name })
};
 
const charactersResponse = await fetchCharacters({
  params: { page: currentPage, ...filters }
});

Filter options are rendered as links that preserve the other active values. Pagination follows the same contract, so moving between pages does not reset the current search.

As a result, every filtered view has a shareable and reload-safe address:

/characters?status=alive&gender=male&name=Rick&page=2

There is no separate client-side store to synchronise with navigation. The URL is both the source of truth for the interface and the input for server-side data fetching.

Connecting related API entities

The Rick and Morty API represents relationships as URLs. For example, a character contains URLs for their origin, current location, and every episode in which they appear.

The detail page converts those URLs into route identifiers and uses them to build navigation across the application:

app/(characters)/character/[id]/page.tsx
const originId = getIdFromUrl('location', character.origin.url);
const locationId = getIdFromUrl('location', character.location.url);
const episodeId = getIdFromUrl('episode', character.episode[0]);
const episodesIds = character.episode.map(
  (url) => +getIdFromUrl('episode', url)
);
 
const episodeResponse = await fetchEpisode({
  params: { id: +episodeId }
});

This creates a graph-like browsing experience on top of a REST API:

Character -> Origin
          -> Current location
          -> First appearance
          -> Every related episode
 
Location  -> Residents
Episode   -> Characters

The approach turns API relationships into meaningful product navigation rather than displaying them as passive metadata.

Normalising inconsistent response shapes

The API returns an object when requesting one character and an array when requesting several. Components should not need to branch based on that transport detail, so the API layer normalises both cases into an array.

src/utils/api/characters.ts
const multipleCharactersResponse = await api.get<Character | Character[]>(
  `/character/${params.multiple}`,
  { params }
);
 
if (Array.isArray(multipleCharactersResponse.data)) {
  const { data } = multipleCharactersResponse;
  return { ...multipleCharactersResponse, data };
}
 
return {
  ...multipleCharactersResponse,
  data: [multipleCharactersResponse.data]
};

Location and episode pages can therefore render related characters with one predictable flow, including edge cases where the API returns only a single result.

Interface and interaction

The visual direction references the show's science-fiction setting without reproducing its interface literally. A restrained green palette, monospace typography, animated particles, and connected lines create a space-like background while leaving character information readable.

The interface also includes:

  • responsive layouts for catalogues and detail pages;
  • compact pagination divided into manageable groups;
  • empty search results instead of an application error;
  • character status indicators;
  • contextual back links;
  • a floating return-to-top action on long pages;
  • navigation states derived from the current route.

Outcome

The completed application covers the entire public data model rather than stopping at a character grid. It combines catalogue browsing, compound filters, pagination, static entity routes, dynamic discovery pages, and cross-entity navigation in one consistent interface.

More importantly, the project provided practical experience with the architectural ideas introduced by the early App Router: choosing rendering behaviour per route, keeping interactive components focused, moving data work to the server, and treating URLs as durable application state.