Introduction
What is X-Cart Frontapp? Frontapp is X-Cart's solution for headless commerce. Built on top of Next.js framework it provides best-in-class templates for creating modern and performant commerce storefronts.
Motivation for Frontapp Over the years of development and use, X-Cart's classic storefront creation solution has proven itself excellently in thousands of successful online stores. There are many quality themes, free or paid, suitable for any type of business. Despite all that, there are some limitations:
- inability to fully use modern front-end technologies;
- suboptimal performance;
- steep learning curve: developers need to master the features of theme architecture;
- tight coupling of themes and business logic.
The best solution to these problems is the so-called headless approach, which separates the frontend and the backend. By providing business logic through an extensive Storefront API, we enable developers to choose any frontend stack that suits them for creating X-Cart storefronts. Frontapp is an opinionated example of implementing such a storefront.
Features
- Professional mobile-first design with light/dark mode based on system settings.
- Built-in widgets and functionality: bestsellers, on sale, recent arrivals, make/model/year filter, shop by brand, garage, wishlist, brands, product variants.
- Full-fledged hosted checkout with integrated X-Payments and Stripe payment methods.
- Integration with CloudSearch & Filters.
- PWA out of the box
Project structure
The project is organized as a monorepo. It includes the following packages:
and two templates:
Template structure:
├── app
│ ├── (checkout)
│ │ └── ...
| ├── (shop)
│ │ └── ...
| ├── client.ts
| ├── layout.tsx
│ └── ...
├── components
│ └── ...
├── pages
| ├── api
| | ├── auth
| | | └── [...nextauth].ts
├── styles
│ └── index.css
├── utils
│ └── ...
├── .env
├── middleware.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── tailwind.config.js
└── tsconfig.json
app/(checkout)
contains checkout-specific routes, splitted into steps, along with checkout UI components.app/(shop)
contains catalog routes, such as product details, categories, search results, etc.app/client.ts
- a client for requests to X-Cart Storefront APIcomponents
contains common UI components, shared across pages (routes)pages/api/auth/[...nextauth].ts
old-fashioned api route for authentication
Getting started
Overview
Frontapp was developed using the Next.js 13 framework. We want to make the most of modern Next.js features such as:
- App Router;
- React Server Components (RSCs) and Suspense;
- Server Actions;
- Fetching and caching paradigms;
- Metadata for SEO.
For this reason, if you do not have experience with this framework, a good start will be to familiarize yourself with the Next.js documentation.
Running locally
- Installing dependencies
yarn install
- Choose the template that you will use -
templates/auto
ortemplates/platform
, create.env.local
in the respective directory and specify the environment variables defined in the.env
file.
- Building
yarn build:auto or yarn build:platform
- Running
yarn auto or yarn platform
Your app should now be running on localhost:3000.
Storefront API Client
Frontapp uses a client from the @xcart/storefront
package for access to the store's Storefront API. The client itself is generated based on the OpenAPI specification. The API documentation for the store some-store.com
can be found at some-store.com/api?section=storefront
. Since the API can vary from store to store, depending on the edition, set of modules or customizations, it may be necessary to regenerate the client. To do this, you need to navigate to the packages/storefront
directory:
cd ./packages/storefront
download the file OpenAPI of the specification (openapi.json) and place it in the directory ./src/gen
. Then run the following:
yarn generate
Example. Getting a product's data by product id:
import {Client} from '@xcart/storefront'
const client = new Client('https://some-store.com', 'STORE_PUBLIC_API_KEY')
const product = await client.product.getDetailedProductById(123)
Authentication
Storefront API can be accessed using a public key that can be found on the page some-store.com/admin/?target=settings&page=API
. In addition, for registered users, an access token needs to be sent in the request Authorization header. An access token (along with a refresh token, see further below) can be obtained via requests to login/registration API endpoints. It needs to be taken into account that the access token expiration time is rather short (20 minutes by default); for this reason, the access token needs to be rotated in a timely manner during a user session with the help of a refresh token. In Frontapp, all token-related logic (acquisition, storage, and renewal) has already been implemented using the NextAuth.js
library. Therefore, it is recommended to use the built-in client from the library app/client.ts
; for example, (snippet from app/(shop)/c/[...slug]/page.tsx
):
import {getXCartClient} from 'app/client'
async function getCategory(cleanUrl: string) {
const client = await getXCartClient()
const category = await client.category.getDetailedCategoryByCleanURL(cleanUrl)
return {category}
}
Data fetching and caching
Fetching
The preferred method of data retrieval is on the server, i.e., in Server Components, in Route Handlers, and in Server Actions. Some of the advantages of this approach include the following:
- The ability to utilize per-request caching provided by the fetch API, which Next.js offers.
- Transmitting only the necessary data to the client, improving performance and reducing dependency on the user's network quality.
- Enhanced security as there is no need to transmit API keys to the client.
Caching
Next.js provides various caching strategies. By default, for the API client, we set the caching time to 1 hour in the file app/client
:
client.setRequestOptions({next: {revalidate: 3600}})
Different resources require different strategies. For instance, infrequently changing data does not need frequent invalidation, so it is possible override the caching strategy at the request level:
import {client as unauthenticatedClient} from 'utils/unauthenticated-client'
const data = await unauthenticatedClient.other.getCountryCollection(
{page: 1, itemsPerPage: 100},
{next: {revalidate: false}},
)
In this example, we are requesting a list of countries and specifying that the response does not require revalidation, thus it will be cached. It's worth noting that here we are using an unauthorized client since the Authentication header in the request (in the case of an authorized customer) automatically disables caching. On the contrary, in cases where we need constantly up-to-date/fresh data, we specify that they should not be cached:
const wishlist = await client.wishlist.getWishlist({next: {revalidate: 0}})
An unexpected behavior that can lead to hard-to-debug bugs and should be taken into account is that PATCH requests are also cached (unlike POST requests). Most of the time, that is not the desired behavior, and for PATCH requests, you should also specify {next: {revalidate: 0}}
:
const res = await client.other.patchCartAddressItem(
String(id),
cartId,
body,
{
next: {revalidate: 0},
},
)
Pagination
The specifics of the Storefront API Client is that it retrieves data in the format application/ld+json
rather than the familiar format application/json
(although the API can also provide data in this format). This is because in collection endpoints, the application/json
response does not contain information about pagination. For example, on a category page, we want to display a list of products divided into pages of 20 items each. The application/json
response does not provide information about the number of products in the category, making it impossible to display the number of pages in the interface.
To the Storefront API Client helper methods for more convenient retrieval of popular paginated lists were added, such as: getPaginatedProducts, getCategoryProducts, getRootCategories, getAllBrandsPaginated, etc.
For various lists the function getSortedPaginatedProducts
from utils/get-sorted-paginated-products.ts
should be used - in addition to built-in pagination, it allows working with filters, sorting, and has built-in support for CloudSearch. For example:
const categoryId = category.id as number
const pageParams = {
searchParams: searchParams ?? {},
categoryId,
}
const products = await getSortedPaginatedProducts(pageParams)
Clean URLs
The specifics of Next.js routing and Storefront API does not allow for an exact replication of the classic X-Cart URL structure. For example, in the classic X-Cart, product and category URLs may look like this:
https://some-store.com/nike-spark-women-s-shoes (product)
https://some-store.com/women/walking-shoes (category)
From a single URL 👆, we cannot determine the type of resource that we need to know in order to make the correct API request. Therefore, in Frontapp, we use prefixes. The URLs above in Frontapp will look like this:
https://some-store.com/nike-spark-women-s-shoes (remains the same)
https://some-store.com/c/women/walking-shoes (prefix /c/
is added)
When developing a storefront for an already existing store, it is important to take care of redirects from the existing URLs to the new ones.
It is also worth noting that, unlike the more common practice of obtaining a single resource by id (e.g., a product for a product details page), more often, resource retrieval is based on clean URLs:
export default async function Page({params: {slug}}) {
const client = await getXCartClient()
const product = await client.product.getDetailedProductByCleanURL(slug)
...
}
This is less efficient from an API performance perspective and will be optimized in the future.
UI / Components
The templates include a multitude of components necessary for the UI of the storefront. These components cover design elements such as buttons, inputs, checkboxes, selects, dropdowns, and more. Additionally, there are more complex components like views, widgets, and entire pages. We've taken care to ensure that the design looks modern and that the templates can be used as-is or with minimal modifications. However, the components are intentionally not released as an official design system and are not published as a separate npm package. This has been done to make customizing the components for your needs as easy as possible – you are free to change or extend their implementation.
You can explore the available set of components by running Storybook in the template directory:
yarn storybook
Styling
For styling, Tailwind CSS is used.
FAQ
Q: How can we change the color palette?
A: Pairs of light/dark colors are defined in the COLORS
object in the file components/theme/colors.ts
.
Deployment
Frontapp can be deployed on any hosting, including edge runtime. However, the recommended approach is to deploy Frontapp alongside the X-Cart backend. In this case, assuming that server fetching is mostly used, better performance can be achieved because the data source (Storefront API) is located nearby.