Connect to the API
The first thing you'll need to do is to connect your storefront app to the Shop API. The Shop API is a GraphQL API that provides access to the products, collections, customer data, and exposes mutations that allow you to add items to the cart, checkout, manage customer accounts, and more.
You can explore the Shop API by opening the GraphQL Playground in your browser at
http://localhost:3000/shop-api
when your Vendure
server is running locally.
Select a GraphQL client
GraphQL requests are made over HTTP, so you can use any HTTP client such as the Fetch API to make requests to the Shop API. However, there are also a number of specialized GraphQL clients which can make working with GraphQL APIs easier. Here are some popular options:
- Apollo Client: A full-featured client which includes a caching layer and React integration.
- urql: The highly customizable and versatile GraphQL client for React, Svelte, Vue, or plain JavaScript
- graphql-request: Minimal GraphQL client supporting Node and browsers for scripts or simple apps
- TanStack Query: Powerful asynchronous state management for TS/JS, React, Solid, Vue and Svelte, which can be combined with
graphql-request
.
Managing Sessions
Vendure supports two ways to manage user sessions: cookies and bearer token. The method you choose depends on your requirements, and is specified by the authOptions.tokenMethod
property of the VendureConfig. By default, both are enabled on the server:
import { VendureConfig } from '@vendure/core';
export const config: VendureConfig = {
// ...
authOptions: {
tokenMethod: ['bearer', 'cookie'],
},
};
Cookie-based sessions
Using cookies is the simpler approach for browser-based applications, since the browser will manage the cookies for you automatically.
-
Enable the
credentials
option in your HTTP client. This allows the browser to send the session cookie with each request.For example, if using a fetch-based client (such as Apollo client) you would set
credentials: 'include'
or if using XMLHttpRequest, you would setwithCredentials: true
-
When using cookie-based sessions, you should set the
authOptions.cookieOptions.secret
property to some secret string which will be used to sign the cookies sent to clients to prevent tampering. This string could be hard-coded in your config file, or (better) reside in an environment variable:src/vendure-config.tsimport { VendureConfig } from '@vendure/core';
export const config: VendureConfig = {
// ...
authOptions: {
tokenMethod: ['bearer', 'cookie'],
cookieOptions: {
secret: process.env.COOKIE_SESSION_SECRET
}
}
}
SameSite cookies
When using cookies to manage sessions, you need to be aware of the SameSite cookie policy. This policy is designed to prevent cross-site request forgery (CSRF) attacks, but can cause problems when using a headless storefront app which is hosted on a different domain to the Vendure server. See this article for more information.
Bearer-token sessions
Using bearer tokens involves a bit more work on your part: you'll need to manually read response headers to get the token, and once you have it you'll have to manually add it to the headers of each request.
The workflow would be as follows:
- Certain mutations and queries initiate a session (e.g. logging in, adding an item to an order etc.). When this happens, the response will contain a HTTP header which by default is called
'vendure-auth-token'
. - So your http client would need to check for the presence of this header each time it receives a response from the server.
- If the
'vendure-auth-token'
header is present, read the value and store it because this is your bearer token. - Attach this bearer token to each subsequent request as
Authorization: Bearer <token>
.
Here's a simplified example of how that would look:
let token: string | undefined = localStorage.getItem('token')
export async function request(query: string, variables: any) {
// If we already know the token, set the Authorization header.
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const response = await someGraphQlClient(query, variables, headers);
// Check the response headers to see if Vendure has set the
// auth token. The header key "vendure-auth-token" may be set to
// a custom value with the authOptions.authTokenHeaderKey config option.
const authToken = response.headers.get('vendure-auth-token');
if (authToken != null) {
token = authToken;
}
return response.data;
}
There are some concrete examples of this approach in the examples later on in this guide.
Session duration
The duration of a session is determined by the AuthOptions.sessionDuration config
property. Sessions will automatically extend (or "refresh") when a user interacts with the API, so in effect the sessionDuration
signifies the
length of time that a session will stay valid since the last API call.
Specifying a channel
If your project has multiple channels, you can specify the active channel by setting
the vendure-token
header on each request to match the channelToken
for the desired channel.
Let's say you have a channel with the token uk-channel
and you want to make a request to the Shop API to get the
products in that channel. You would set the vendure-token
header to uk-channel
:
export function query(document: string, variables: Record<string, any> = {}) {
return fetch('https://localhost:3000/shop-api', {
method: 'POST',
headers: {
'content-type': 'application/json',
'vendure-token': 'uk-channel',
},
credentials: 'include',
body: JSON.stringify({
query: document,
variables,
}),
})
.then((res) => res.json())
.catch((err) => console.log(err));
}
If no channel token is specified, then the default channel will be used.
The header name vendure-token
is the default, but can be changed using the apiOptions.channelTokenKey
config option.
Setting language
If you have translations of your products, collections, facets etc, you can specify the language for the request by setting the languageCode
query string on the request. The value should be one of the ISO 639-1 codes defined by the LanguageCode
enum.
POST http://localhost:3000/shop-api?languageCode=de
Code generation
If you are building your storefront with TypeScript, we highly recommend you set up code generation to ensure that the responses from your queries & mutation are always correctly typed according the fields you request.
See the GraphQL Code Generation guide for more information.
Examples
Here are some examples of how to set up clients to connect to the Shop API. All of these examples include functions for setting the language and channel token.
Fetch
First we'll look at a plain fetch-based implementation, to show you that there's no special magic to a GraphQL request - it's just a POST request with a JSON body.
Note that we also include a React hook in this example, but that's just to make it more convenient to use the client in a React component - it is not required.
- client.ts
- App.tsx
- index.ts
import { useState, useEffect } from 'react';
// If using bearer-token based session management, we'll store the token
// in localStorage using this key.
const AUTH_TOKEN_KEY = 'auth_token';
const API_URL = 'https://readonlydemo.vendure.io/shop-api';
let languageCode: string | undefined;
let channelToken: string | undefined;
export function setLanguageCode(value: string | undefined) {
languageCode = value;
}
export function setChannelToken(value: string | undefined) {
channelToken = value;
}
export function query(document: string, variables: Record<string, any> = {}) {
const authToken = localStorage.getItem(AUTH_TOKEN_KEY);
const headers = new Headers({
'content-type': 'application/json',
});
if (authToken) {
headers.append('authorization', `Bearer ${authToken}`);
}
if (channelToken) {
headers.append('vendure-token', channelToken);
}
let endpoint = API_URL;
if (languageCode) {
endpoint += `?languageCode=${languageCode}`;
}
return fetch(endpoint, {
method: 'POST',
headers,
credentials: 'include',
body: JSON.stringify({
query: document,
variables,
}),
}).then((res) => {
if (!res.ok) {
throw new Error(`An error ocurred, HTTP status: ${res.status}`);
}
const newAuthToken = res.headers.get('vendure-auth-token');
if (newAuthToken) {
localStorage.setItem(AUTH_TOKEN_KEY, newAuthToken);
}
return res.json();
});
}
/**
* Here we have wrapped the `query` function into a React hook for convenient use in
* React components.
*/
export function useQuery(
document: string,
variables: Record<string, any> = {}
) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
query(document, variables)
.then((result) => {
setData(result.data);
setError(null);
})
.catch((err) => {
setError(err.message);
setData(null);
})
.finally(() => {
setLoading(false);
});
}, []);
return { data, loading, error };
}
import { useQuery } from './client';
import './style.css';
const GET_PRODUCTS = /*GraphQL*/ `
query GetProducts($options: ProductListOptions) {
products(options: $options) {
items {
id
name
slug
featuredAsset {
preview
}
}
}
}
`;
export default function App() {
const { data, loading, error } = useQuery(GET_PRODUCTS, {
options: { take: 3 },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error : {error.message}</p>;
return data.products.items.map(({ id, name, slug, featuredAsset }) => (
<div key={id}>
<h3>{name}</h3>
<img src={`${featuredAsset.preview}?preset=small`} alt={name} />
</div>
));
}
import * as React from 'react';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(
<StrictMode>
<App />
</StrictMode>
);
Here's a live version of this example:
As you can see, the basic implementation with fetch
is quite straightforward. However, it is also lacking some features that other,
dedicated client libraries will provide.
Apollo Client
Here's an example configuration for Apollo Client with a React app.
Follow the getting started instructions to install the required packages.
- client.ts
- index.tsx
- App.tsx
import {
ApolloClient,
ApolloLink,
HttpLink,
InMemoryCache,
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
const API_URL = `https://demo.vendure.io/shop-api`;
// If using bearer-token based session management, we'll store the token
// in localStorage using this key.
const AUTH_TOKEN_KEY = 'auth_token';
let channelToken: string | undefined;
let languageCode: string | undefined;
const httpLink = new HttpLink({
uri: () => {
if (languageCode) {
return `${API_URL}?languageCode=${languageCode}`;
} else {
return API_URL;
}
},
// This is required if using cookie-based session management,
// so that any cookies get sent with the request.
credentials: 'include',
});
// This part is used to check for and store the session token
// if it is returned by the server.
const afterwareLink = new ApolloLink((operation, forward) => {
return forward(operation).map((response) => {
const context = operation.getContext();
const authHeader = context.response.headers.get('vendure-auth-token');
if (authHeader) {
// If the auth token has been returned by the Vendure
// server, we store it in localStorage
localStorage.setItem(AUTH_TOKEN_KEY, authHeader);
}
return response;
});
});
/**
* Used to specify a channel token for projects that use
* multiple Channels.
*/
export function setChannelToken(value: string | undefined) {
channelToken = value;
}
/**
* Used to specify a language for any localized results.
*/
export function setLanguageCode(value: string | undefined) {
languageCode = value;
}
export const client = new ApolloClient({
link: ApolloLink.from([
// If we have stored the authToken from a previous
// response, we attach it to all subsequent requests.
setContext((request, operation) => {
const authToken = localStorage.getItem(AUTH_TOKEN_KEY);
let headers: Record<string, any> = {};
if (authToken) {
headers.authorization = `Bearer ${authToken}`;
}
if (channelToken) {
headers['vendure-token'] = channelToken;
}
return { headers };
}),
afterwareLink,
httpLink,
]),
cache: new InMemoryCache(),
});
import React from 'react';
import * as ReactDOM from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import App from './App';
import { client } from './client';
// Supported in React 18+
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
);
import { useQuery, gql } from '@apollo/client';
const GET_PRODUCTS = gql`
query GetProducts($options: ProductListOptions) {
products(options: $options) {
items {
id
name
slug
featuredAsset {
preview
}
}
}
}
`;
export default function App() {
const { loading, error, data } = useQuery(GET_PRODUCTS, {
variables: { options: { take: 3 } },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error : {error.message}</p>;
return data.products.items.map(({ id, name, slug, featuredAsset }) => (
<div key={id}>
<h3>{name}</h3>
<img src={`${featuredAsset.preview}?preset=small`} alt={name} />
</div>
));
}
Here's a live version of this example:
TanStack Query
Here's an example using @tanstack/query in combination with graphql-request based on this guide.
Note that in this example we have also installed the @graphql-typed-document-node/core
package, which allows the
client to work with TypeScript code generation for type-safe queries.
- client.ts
- App.tsx
- index.tsx
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
import {
GraphQLClient,
RequestDocument,
RequestMiddleware,
ResponseMiddleware,
Variables,
} from 'graphql-request';
// If using bearer-token based session management, we'll store the token
// in localStorage using this key.
const AUTH_TOKEN_KEY = 'auth_token';
const API_URL = 'http://localhost:3000/shop-api';
// If we have a session token, add it to the outgoing request
const requestMiddleware: RequestMiddleware = async (request) => {
const authToken = localStorage.getItem(AUTH_TOKEN_KEY);
return {
...request,
headers: {
...request.headers,
...(authToken ? { authorization: `Bearer ${authToken}` } : {}),
},
};
};
// Check all responses for a new session token
const responseMiddleware: ResponseMiddleware = (response) => {
if (!(response instanceof Error) && !response.errors) {
const authHeader = response.headers.get('vendure-auth-token');
if (authHeader) {
// If the session token has been returned by the Vendure
// server, we store it in localStorage
localStorage.setItem(AUTH_TOKEN_KEY, authHeader);
}
}
};
const client = new GraphQLClient(API_URL, {
// Required for cookie-based sessions
credentials: 'include',
requestMiddleware,
responseMiddleware,
});
/**
* Sets the languageCode to be used for all subsequent requests.
*/
export function setLanguageCode(languageCode: string | undefined) {
if (!languageCode) {
client.setEndpoint(API_URL);
} else {
client.setEndpoint(`${API_URL}?languageCode=${languageCode}`);
}
}
/**
* Sets the channel token to be used for all subsequent requests.
*/
export function setChannelToken(channelToken: string | undefined) {
if (!channelToken) {
client.setHeader('vendure-token', undefined);
} else {
client.setHeader('vendure-token', channelToken);
}
}
/**
* Makes a GraphQL request using the `graphql-request` client.
*/
export function request<T, V extends Variables = Variables>(
document: RequestDocument | TypedDocumentNode<T, V>,
variables: Record<string, any> = {}
) {
return client.request(document, variables);
}
import * as React from 'react';
import { gql } from 'graphql-request';
import { useQuery } from '@tanstack/react-query';
import { request } from './client';
const GET_PRODUCTS = gql`
query GetProducts($options: ProductListOptions) {
products(options: $options) {
items {
id
name
slug
featuredAsset {
preview
}
}
}
}
`;
export default function App() {
const { isLoading, data } = useQuery({
queryKey: ['products'],
queryFn: async () =>
request(GET_PRODUCTS, {
options: { take: 3 },
}),
});
if (isLoading) return <p>Loading...</p>;
return data ? (
data.products.items.map(({ id, name, slug, featuredAsset }) => (
<div key={id}>
<h3>{name}</h3>
<img src={`${featuredAsset.preview}?preset=small`} alt={name} />
</div>
))
) : (
<>Loading...</>
);
}
import * as React from 'react';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
// Create a client
const queryClient = new QueryClient();
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</StrictMode>
);
Here's a live version of this example: