Skip to main content

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.

tip

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:

src/vendure-config.ts
import { VendureConfig } from '@vendure/core';

export const config: VendureConfig = {
// ...
authOptions: {
tokenMethod: ['bearer', 'cookie'],
},
};

Using cookies is the simpler approach for browser-based applications, since the browser will manage the cookies for you automatically.

  1. 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 set withCredentials: true

  2. 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.ts
    import { VendureConfig } from '@vendure/core';

    export const config: VendureConfig = {
    // ...
    authOptions: {
    tokenMethod: ['bearer', 'cookie'],
    cookieOptions: {
    secret: process.env.COOKIE_SESSION_SECRET
    }
    }
    }
caution

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:

  1. 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'.
  2. So your http client would need to check for the presence of this header each time it receives a response from the server.
  3. If the 'vendure-auth-token' header is present, read the value and store it because this is your bearer token.
  4. 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:

src/client.ts
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));
}
note

If no channel token is specified, then the default channel will be used.

info

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.

src/client.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 };
}

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.

src/client.ts
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(),
});

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.

src/client.ts
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);
}

Here's a live version of this example: