Skip to content
Client
Server

Set up your client ​

Your page calls Apple with a client that fetches its developer token from your server. In this guide you add the SDK to your page, let it reach your server, make its one client, and make a first request.

Before you start, you need:

The code below is for the client you pick in the nav: a page with no framework, a React app on Vite, or a Next.js app.

1. Install the package ​

sh
pnpm add @open-music-sdk/browser
pnpm add -D vite

Vite serves the page while you work, and builds it.

sh
pnpm add @open-music-sdk/react @tanstack/react-query react react-dom
pnpm add -D vite @vitejs/plugin-react

Vite serves the page while you work, and builds it. @open-music-sdk/react doesn't depend on TanStack Query. It gives the objects TanStack Query's hooks take, so the examples here use it, as the React sample does.

You installed @open-music-sdk/react with the server's package in Set up your server. Add TanStack Query, whose hooks the examples here use:

sh
pnpm add @tanstack/react-query

2. Let it reach your server ​

Serve the page from the origin your server's developer tokens are bound to, its PAGE_ORIGIN, and forward /api to your server, so that the page and its server share one origin. The vanilla sample's vite.config.ts does both, and refuses to serve your key or any other secret in the repository to a page that asks:

ts
import { defineConfig } from "vite";

// The page is served on localhost:2728, the origin the servers bind the
// developer token to, and a port of its own: Apple's script keeps the
// listener's token in this origin's storage, which another app on the same port
// could read. Everything under /api goes to whichever server sample runs on
// 8787, so the page and its server share one origin.
export default defineConfig({
  server: {
    port: 2728,
    strictPort: true,
    proxy: { "/api": "http://127.0.0.1:8787" },
    // Vite's dev server can hand out any file in the repository to a page that
    // asks. Its default list of refusals is restated, with a MusicKit key and
    // Wrangler's files added, and no page on another origin may read an answer.
    cors: false,
    fs: {
      deny: [
        ".env",
        ".env.*",
        "*.{crt,pem,key,p12,pfx,cer,der}",
        ".npmrc",
        ".yarnrc.yml",
        "**/.git/**",
        "*.p8",
        ".dev.vars",
        ".dev.vars.*",
        "**/.wrangler/**",
      ],
    },
    // No page may frame this one. Another localhost port counts as the same
    // site, so a frame there would have the listener signed in, and a page
    // opened by its address could be laid under a click meant for something
    // else.
    headers: {
      "X-Frame-Options": "DENY",
      "Content-Security-Policy": "frame-ancestors 'none'",
    },
  },
});

Port 2728 is the page's own. Apple's script keeps a listener's token in the storage of the page's origin, which another app served on the same port could read.

Serve the page from the origin your server's developer tokens are bound to, its PAGE_ORIGIN, and forward /api to your server, so that the page and its server share one origin. The React sample's vite.config.ts does both, with React's plugin, and refuses to serve your key or any other secret in the repository to a page that asks:

ts
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

// The page is served on localhost:2728, the origin the servers bind the
// developer token to, and a port of its own: Apple's script keeps the
// listener's token in this origin's storage, which another app on the same port
// could read. Everything under /api goes to whichever server sample runs on
// 8787, so the page and its server share one origin.
export default defineConfig({
  // React's own plugin: components swapped in place as they are edited.
  plugins: [react()],
  server: {
    port: 2728,
    strictPort: true,
    proxy: { "/api": "http://127.0.0.1:8787" },
    // Vite's dev server can hand out any file in the repository to a page that
    // asks. Its default list of refusals is restated, with a MusicKit key and
    // Wrangler's files added, and no page on another origin may read an answer.
    cors: false,
    fs: {
      deny: [
        ".env",
        ".env.*",
        "*.{crt,pem,key,p12,pfx,cer,der}",
        ".npmrc",
        ".yarnrc.yml",
        "**/.git/**",
        "*.p8",
        ".dev.vars",
        ".dev.vars.*",
        "**/.wrangler/**",
      ],
    },
    // No page may frame this one. Another localhost port counts as the same
    // site, so a frame there would have the listener signed in, and a page
    // opened by its address could be laid under a click meant for something
    // else.
    headers: {
      "X-Frame-Options": "DENY",
      "Content-Security-Policy": "frame-ancestors 'none'",
    },
  },
});

Port 2728 is the page's own. Apple's script keeps a listener's token in the storage of the page's origin, which another app served on the same port could read.

A Next app is its own server. Its pages and its routes share one origin, so the token route is at /api/music/token, with nothing to forward. Run it on the port of PAGE_ORIGIN, on your machine alone:

sh
pnpm next dev --port 2728 --hostname localhost

3. Make the page's client once ​

Make one client for the page, in a module the rest of the page imports. In src/music.ts:

ts
import { createBrowserClient } from "@open-music-sdk/browser";

export const music = createBrowserClient({
  tokenUrl: "/api/music/token",
  storefront: "us",
});

Make the client once, outside the tree, and hand it to MusicProvider, inside TanStack Query's QueryClientProvider. In src/main.tsx:

tsx
import { MusicProvider, createBrowserClient } from "@open-music-sdk/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRoot } from "react-dom/client";
import { App } from "./app";

const music = createBrowserClient({
  tokenUrl: "/api/music/token",
  storefront: "us",
});
const queryClient = new QueryClient();

createRoot(document.getElementById("root")!).render(
  <QueryClientProvider client={queryClient}>
    <MusicProvider music={music}>
      <App />
    </MusicProvider>
  </QueryClientProvider>,
);

Components get the client from the provider, with useMusic().

Client components get their client from MusicProvider. Make the page's client once, outside the tree, in a client component of its own. In src/components/providers.tsx:

tsx
"use client";
import { MusicProvider, createBrowserClient } from "@open-music-sdk/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState, type ReactNode } from "react";

const music = createBrowserClient({
  tokenUrl: "/api/music/token",
  storefront: "us",
});

export function Providers({ children }: { children: ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  return (
    <QueryClientProvider client={queryClient}>
      <MusicProvider music={music}>{children}</MusicProvider>
    </QueryClientProvider>
  );
}

Then wrap your layout in it. In src/app/layout.tsx:

tsx
import type { ReactNode } from "react";
import { Providers } from "@/components/providers";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

The provider holds the listener's token in React state, in memory only. Never hand it a token as a prop from a server component: a prop from a server component is written into the page. The token arrives through useConnect, as Sign a listener in shows.

storefront is the storefront the catalog is read in. The client fetches a developer token from tokenUrl the first time a request needs one, and fetches the next before that one runs out.

4. Make a first request ​

Read an album from the catalog, and show its name. In src/main.ts:

ts
import { music } from "./music";

const album = await music.catalog.getAlbum("1613600183");
const heading = document.querySelector("h1");
if (heading) heading.textContent = album.attributes?.name ?? "";

In a component, useMusicQueries() gives every read function of the catalog as a query:

tsx
import { useMusicQueries } from "@open-music-sdk/react";
import { useQuery } from "@tanstack/react-query";

export function Album({ id }: { id: string }) {
  const queries = useMusicQueries();
  const { data: album } = useQuery(queries.catalog.getAlbum(id));
  return <h1>{album?.attributes?.name}</h1>;
}

Render it from App, as <Album id="1613600183" />. Read and write with TanStack Query goes on from here: walks through pages, writes, and what the keys hold.

A server component reads the catalog with the server's client, which signs its own token. A new Next 16 app has Cache Components turned on, so a read made at each request goes inside <Suspense>, after await connection(). In src/app/album/[id]/page.tsx:

tsx
import { connection } from "next/server";
import { Suspense } from "react";
import { getMusic } from "@/lib/music";

type tParams = Promise<{ id: string }>;

export default function AlbumPage({ params }: { params: tParams }) {
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <Album params={params} />
    </Suspense>
  );
}

async function Album({ params }: { params: tParams }) {
  await connection();
  const { id } = await params;
  const album = await getMusic().catalog.getAlbum(id);
  return <h1>{album.attributes?.name}</h1>;
}

The client reads the clock to keep its developer token fresh. Next refuses that while it draws a page ahead of time, and stops with an error that names Date.now(), so await connection() makes the read wait for a request. The page's own client is for what runs in the browser: the listener's sign-in, previews, and queries.

Use server actions and caching in Next.js covers server actions that act for a listener, and catalog reads cached for everyone. Read and write with TanStack Query prefetches the catalog for a client component's query.

5. Open it ​

Give the page an index.html beside package.json, with the heading the code fills in, and start Vite:

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>An album</title>
  </head>
  <body>
    <h1></h1>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
sh
pnpm vite

Give the page an index.html beside package.json, with the element React draws into, and start Vite:

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>An album</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
sh
pnpm vite

next dev, from step 2, serves the page.

Open http://localhost:2728, with your server running too. The album's name on the page means the whole path works, from your key to Apple.

Next steps ​

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