Fun with Remix, React and Discogs API

I'd like to share and hopefully inspire some of you to do more side projects.

This is how I thought about building a vinyl list web app, the process of building it, including "some" not all technical details.

First some context.

I had a "need" and an itch to get some game time with Remix.

Best way to learn is to build something, preferably something with/around something that you are care about.

It is no secret that I am a huge fan of music. I have been collecting and playing records over the years, on and off... and right now very much on.

I found a local record store that I like Filter Musikk, I have been buying records from them for the last little while now.

They have a Discogs store online. Discogs is marketplace for music (vinyl, cd's, cassettes) it's like eBay for music, but better.

Anyways, lets talk about code the Discogs API and how I have been playing around with it.

I thought it would be cool to build a little app that would allow me to see what they have in stock.

๐ŸŽน Live demo here

Image description

The data

I like to save my endpoints in collections in Postman, so I can easily refer to them later.

Here is the Discogs API endpoint I used to get the inventory of a user.

https://api.discogs.com/users/username/inventory
?status=status
&sort=sort
&sort_order=sort_order
&per_page=per_page
&page=page

Fairly simple.

Discogs API collection

The plan and design

Keep it clean simple design. Focus on the music, the records and the artwork.

Discogs App design

The stack

Remix for the server side rendering. I was only concerned with learning how things work in Remix with loaders, routes and actions. So I did not build anything from scratch.

I used Tailwind CSS for the styling because it's fast and easy to use, after a second thought I decided to use shadcn/ui so I did not have to build the components from scratch either.

The code

Make use of the Remix resources for building the app, no sense reinventing the wheel.

I used the server side pagination by Jacob Paris.

Type definitions

I like to get accustomed to the data I am working with, so I define the types I will be working with. Quicktype.io is good for this job.

export interface Pagination {
  items: number;
  page: number;
  pages: number;
  per_page: number;
  urls: Record<
    string,
    {
      last: string;
      next: string;
    }
  >;
}

export interface Price {
  currency: string;
  value: number;
}

interface OriginalPrice {
  curr_abbr: string;
  curr_id: number;
  formatted: string;
  value: number;
}

interface SellerStats {
  rating: string;
  stars: number;
  total: number;
}

interface Seller {
  id: number;
  username: string;
  avatar_url: string;
  stats: SellerStats;
  min_order_total: number;
  html_url: string;
  uid: number;
  url: string;
  payment: string;
  shipping: string;
  resource_url: string;
}

interface Image {
  type: string;
  uri: string;
  resource_url: string;
  uri150: string;
  width: number;
  height: number;
}

interface ReleaseStatsCommunity {
  in_wantlist: number;
  in_collection: number;
}

interface ReleaseStats {
  community: ReleaseStatsCommunity;
}

interface Release {
  thumbnail: string;
  description: string;
  images: Image[];
  artist: string;
  format: string;
  resource_url: string;
  title: string;
  year: number;
  id: number;
  label: string;
  catalog_number: string;
  stats: ReleaseStats;
}

export interface Listing {
  id: number;
  resource_url: string;
  uri: string;
  status: string;
  condition: string;
  sleeve_condition: string;
  comments: string;
  ships_from: string;
  posted: string;
  allow_offers: boolean;
  offer_submitted: boolean;
  audio: boolean;
  price: Price;
  original_price: OriginalPrice;
  shipping_price: Record<string, unknown>;
  original_shipping_price: Record<string, unknown>;
  seller: Seller;
  release: Release;
}

export interface InventoryFetchResponse {
  pagination: Pagination;
  listings: Listing[];
}

Inventory server component

import type { LoaderFunction, MetaFunction } from "@remix-run/node";
import { json, useLoaderData } from "@remix-run/react";
import { Footer } from "~/components/footer";
import { PaginationBar } from "~/components/paginationBar";
import { StatusAlert } from "~/components/StatusAlert";
import { fetchUserInventory } from "~/inventory";
import { Inventory } from "~/inventory/inventory";

export const loader: LoaderFunction = async ({ request }) => {
  const url = new URL(request.url);
  const searchParams = new URLSearchParams(url.search);
  const pageNumber = searchParams.get("page") || "1";

  try {
    const data = await fetchUserInventory(
      pageNumber,
      "12",
      process.env.SELLER_USERNAME,
      "for sale",
      "listed",
      "desc",
    );

    return json({ inventory: data, ENV: { sellerUsername: process.env.SELLER_USERNAME } });
  } catch (error) {
    console.log("Error fetching inventory:", error);
    return json({ error: "Failed to load inventory. Please try again later or" }, { status: 500 });
  }
};

export const Meta: MetaFunction = () => {
  const { ENV } = useLoaderData<typeof loader>();
  return [
    {
      title: `Shop ${ENV.sellerUsername} records`,
    },
    {
      name: "description",
      content: `Buy some vinyl records from ${ENV.sellerUsername}`,
    },
  ];
};

export default function Index() {
  const { inventory, error } = useLoaderData<typeof loader>();

  if (error) {
    <StatusAlert {...error} />;
  }

  if (!inventory || !inventory.pagination) {
    return <div>No inventory data available.</div>;
  }

  return (
    <>
      <Inventory {...inventory} />
      <PaginationBar total={inventory.pagination.pages} />
      <Footer />
    </>
  );
}

The Inventory component takes the inventory data and renders it, it is the entry point for the app index page, no routes planned.

I show an alert if there is an error fetching the data, with a link to Discogs api status page.

Remix works like this:

  • you have a loader function that fetches the data server side and returns it to the component wrapped by json() function

  • the default Index function then renders the data

  • meta function is used to set the title and description of the page.

fetchUserInventory function

export const loader: LoaderFunction = async ({ request }) => {
  const url = new URL(request.url);
  const searchParams = new URLSearchParams(url.search);
  const pageNumber = searchParams.get("page") || "1";

  try {
    const data = await fetchUserInventory(
      pageNumber,
      "12",
      process.env.SELLER_USERNAME,
      "for sale",
      "listed",
      "desc",
    );

    return json({ inventory: data, ENV: { sellerUsername: process.env.SELLER_USERNAME } });
  } catch (error) {
    console.log("Error fetching inventory:", error);
    return json({ error: "Failed to load inventory. Please try again later or" }, { status: 500 });
  }
};
  • this function is used to fetch the data from the Discogs API,

  • it takes the page number, number of items per page, seller username, status, condition, sort order as arguments.

  • it returns the data and the seller username to the loader function.

  • returns an error if there is an error fetching the data.

Inventory client component

The card component is your typical React card component, it has a header, content and footer.

I build string of the artist and song title and pass it to the link, which when clicked launches YouTube Music, its not perfect. Some times it does not find the song, but it works most of the time.

import React from "react";
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "~/components/ui/card";
import { cn } from "~/lib/utils";
import type { InventoryFetchResponse, Listing } from "./inventory.types";

export const Inventory = (data: InventoryFetchResponse): React.ReactElement => {
  return (
    <section className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6 gap-4 justify-items-start pb-7">
      {data.listings.map((listing: Listing) => (
        <article
          id={listing.release.title}
          key={listing.release.title}
          className="flex justify-start w-full"
        >
          <Card className={cn("p-0 shadow-none w-full overflow-hidden")}>
            <div className="justify-items-start">
              <CardHeader
                className="flex-1 h-40 sm:h-60 md:h-90 lg:h-100 p-0 relative"
                title={listing.release.title}
                style={{
                  backgroundImage: listing.release.images[0]
                    ? `url(${listing.release.images[0].uri})`
                    : "",
                  backgroundSize: "cover",
                  backgroundPosition: "center",
                }}
              >
                {listing.condition ?? listing.condition}
              </CardHeader>
              <CardContent className="flex-1 pt-4">
                <CardTitle className="text-sm">
                  {listing.release.title}
                </CardTitle>
                <CardDescription className="leading-6 text-black">
                  <strong>Artist:</strong> {listing.release.artist}
                  <br />
                  <strong>Label:</strong> {listing.release.label} -{" "}
                  {listing.release.catalog_number}
                  <br />
                  <strong>Released:</strong> {listing.release.year}
                  <br />
                  <strong>Price:</strong> {listing.original_price.formatted}
                </CardDescription>
                <CardDescription className="leading-6">
                  <span className="grid grid-cols-1 md:grid-cols-2 gap-1">
                    <a
                      href={listing.uri}
                      target="_blank"
                      rel="noopener noreferrer"
                      className="text-xs text-white border-0 border-black-600 bg-black hover:text-color-black rounded-md p-2 mt-2 "
                    >
                      View on Discogs
                    </a>
                    <a
                      href={`https://music.youtube.com/search?q=${
                        encodeURIComponent(
                          listing.release.title,
                        )
                      } ${encodeURIComponent(listing.release.artist)}`}
                      target="_blank"
                      rel="noopener noreferrer"
                      className="flex justify-start text-xs text-white border-0 border-black-600 bg-black hover:text-color-black rounded-md p-2 mt-2"
                    >
                      <span className="w-[24px] mt-[2px]">
                        <img
                          src={"./icon-youtube.svg"}
                          alt="YouTube"
                          width={16}
                          height={16}
                        />
                      </span>
                      <span>Listen</span>
                    </a>
                  </span>
                </CardDescription>
              </CardContent>
            </div>
            <CardFooter className="p-0"></CardFooter>
          </Card>
        </article>
      ))}
    </section>
  );
};

The result

Live demo here

Discogs App result

Conclusion

I enjoy the plug and play nature of Remix and the freedom to do as I please.

The documentation is good and easy to follow, it's easy to get started and build something quickly.

The data flow concept is easy to understand makes doing SSR much more approachable than it was in the past.

Data fetching with the loader and hooks to get the data out its concise, did not get the chance yet to use an action as I will need to build form (coming up next a site search).

Remix focus on performance and Web standards, which I like. Did not reach for Axios or anything like that (TanStack query) because I did not need to, the fetch API is good enough for simple tasks like this.

When it Remix merges into React Router 7 I will continue to use it, I will take this on a case by case basis, depending on the project and types of problems we trying to solve.

Looking else where/alternatives would be to use NextJS or maybe look at react server components next.

Ask me again in the future when React 19 is out.

Get the code on GitHub.

0
Subscribe to my newsletter

Read articles from Mannuel Ferreira directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mannuel Ferreira
Mannuel Ferreira

I am a Software Engineer from South Africa, now living in Oslo. I a teaching Front End Development at Noroff Fagskole here in Oslo. I love building web applications.