Skip to content
Client
Server

This guide is for Next.js, which you haven't picked.

Use server actions and caching in Next.js ​

In a Next.js app, your server can do what a page would otherwise do for itself. In this guide you act for a listener in a server action, and cache the catalog reads that are the same for everyone.

Before you start, you need:

  • a client on your server that finds each listener's token in a store, made on first use rather than as its module loads, since next build loads it with no key. See Set up your server.
  • a route that stores each listener's token under your own session's id. See Keep a listener's token on your server.

In the code below, getMusic() returns that client. sessionId() is your own session lookup: it resolves to the signed-in user's id, or undefined when there's none. explain(e) puts an Apple Music error in words the listener can act on.

The Next sample acts and caches this way, and the code below comes from it.

1. Act for a listener in a server action ​

A server action finds the listener's token in your store with getMusic().forUser(userId). Write it in a file that starts with "use server", here src/lib/actions.ts.

In the code below, libraryIds(kind, id) checks what the page sent, and returns the ids addToLibrary takes, such as { songs: [id] }, or undefined. SIGN_IN_FIRST is the answer { ok: false, said: "Sign in with Apple Music first." }, and tAnswer<T> is { ok: true, value: T } or { ok: false, said: string }. isAppleMusicError and the type tMusic come from @open-music-sdk/server:

ts
/**
 * The client that acts for this request's listener. It finds their token in
 * the store at each request; without one, Apple's answer is a
 * UserTokenInvalid.
 */
async function forListener(): Promise<tMusic | undefined> {
  const session = await sessionId();
  if (session === undefined) return undefined;
  return getMusic().forUser(session);
}

/** Runs the work, and answers Apple's error in words rather than throw it. */
async function answer<T>(work: () => Promise<T>): Promise<tAnswer<T>> {
  try {
    return { ok: true, value: await work() };
  } catch (e) {
    if (isAppleMusicError(e)) return { ok: false, said: explain(e) };
    throw e;
  }
}

/**
 * Adds a song, a music video or an album to the listener's library. Apple
 * accepts the request at once, and adds it afterwards, within seconds.
 */
export async function addToLibrary(
  kind: "songs" | "music-videos" | "albums",
  id: string,
): Promise<tAnswer> {
  const ids = libraryIds(kind, id);
  if (ids === undefined) throw new TypeError("addToLibrary: not an id");
  const listener = await forListener();
  if (listener === undefined) return SIGN_IN_FIRST;
  return answer(async () => {
    await listener.user.addToLibrary(ids);
  });
}

The client looks the token up in the store at each request. A listener the store has no token for gets a UserTokenInvalid error, as does one whose token Apple answers 403 to. Either way, ask them to sign in again.

answer hands back Apple's error in words, as { ok: false, said }, rather than throwing it: in production, Next keeps a thrown error's message from the page. The page says what the action answered.

Every server action is a POST that anyone can send. Check each argument before you act, as libraryIds does here, and act only for the session that sent the request.

A listener who isn't signed in to Apple Music, or has no subscription, can get a 401 instead, which arrives as DeveloperTokenInvalid. validateUserToken tells it apart from a developer token Apple refuses. See Handle errors.

2. Cache catalog reads ​

The client sends every request through the fetch it's given, so Next's caching applies the usual ways: pass a fetch of your own to createServerClient, or wrap a read in a function marked "use cache". In a server module, here src/lib/catalog.ts, where cacheLife, from next/cache, sets how long Next keeps what the function gives:

ts
/** A storefront's top charts: the first of each type is its most played. */
export async function topCharts(storefront: string) {
  "use cache";
  cacheLife("hours");
  const { results } = await getMusic().catalog.getCharts({
    types: ["songs", "albums", "playlists"],
    limit: 20,
    storefront,
  });
  return results;
}

A read made for each request, as a page is drawn, needs await connection(), from next/server, before it: the client reads the clock to keep its developer token fresh, which a page Next draws ahead may not. A read marked "use cache" needs none: Next runs it once and keeps what it gives. It needs Cache Components, turned on in next.config.ts:

ts
import type { NextConfig } from "next";

const config: NextConfig = {
  cacheComponents: true,
};

export default config;

See Next's documentation.

Cache catalog reads, which are the same for everyone in a storefront. A listener's library is theirs alone, so don't cache it this way.

Next steps ​

MIT licensed. Not affiliated with, endorsed by, or sponsored by Apple Inc. Apple Music and MusicKit are trademarks of Apple Inc.