Commercify anything: The PokéMart Express!
It’s 5:00 p.m. and I am in a pub in Helsinki after a great workshop with one of our solution partners. That kind of workshop that starts triggering ideas. I heard someone saying that a basic architecture is simply Commerce Layer and a CMS. That’s it, the minimum setup.
Well, I don’t think that’s necessarily true.
Commerce Layer helps brands and retailers to sell any product across any channels, in any location.
In other words, I can make anything I want shoppable.
What if I had some data exposed by an API, maybe legacy, something I might intend to migrate? Even with this, I can still start selling immediately and create a fully-functional ecommerce website with just that legacy stuff and Commerce Layer. Side-note: You might not even need a website with Commerce Layer’s Links, but that’s a story for a different time.
Since I first learned of Commerce Layer, I have always wanted to create a foundational website that uses Commerce Layer at its core. I wanted to build something for iteration and experimental stuff. I wanted a site where I could publish different use cases. That time has come...
Goal
We will build a website selling Pokémons using a public API to feed our content and Commerce Layer to sell them.
Components
To build this demo, we will use the following services:
- Commerce Layer: A headless commerce platform and the backbone of your ecommerce, handling all commerce functionalities like product management, cart, and orders.
- Next.js: A React framework for building static and dynamic websites and web applications, offering server-side rendering and generating static websites for projects like our Pokémon store.
- Vercel: A platform for hosting and deploying your Next.js projects. It provides seamless integration with Next.js, ensuring easy deployments and automatic scaling.
- PokéAPI: An open API for Pokémon data. We will use this to pull Pokémon information into the store.
Too see the final outcome of what we are about to build, follow this link.
Commerce Layer
First of all, create a Commerce Layer account.
Sign up for free and you will be able to create your organization, I called mine “PokéMart Express” 🤓. At this point do not follow the rest of the tutorial seeding it with data, we’ll manually add what we need later.
Next.js
Let’s create our Next.js website. Following the Next.js documentation, we'll set up our project environment. I’m all in for TypeScript and Tailwind for styling.
npx create-next-app@latest --typescript
And hey, I'm a fan of pnpm, but feel free to use npm or yarn, whatever floats your boat.
Vercel
After you've got your Next.js website up and running and pushed the repo to your favorite git provider, it's time to make it live. Head over to Vercel and sign up or log in. Then connect your repository.
Data
As I said, I’ll use PokeAPI for this. The point here is that it could be any API. Specifically, I will use a TS wrapper for it.
Let’s start by getting a PDP (Pokemon Detail Page) up and running:
- Create the
pages/pokemon/[slug].tsx
file. - Install
pokenode-ts
pnpm install axios axios-cache-interceptor pokenode-ts
- Use the PokemonClient to get Pokemon information using the slug:
import { cache } from "react";
import { PokemonClient, Pokemon } from "pokenode-ts";
export const getData = cache(
async (paramSlug?: string | string[]): Promise<Pokemon | null> => {
if (!paramSlug) {
return null;
}
const slug = Array.isArray(paramSlug) ? paramSlug[0] : paramSlug;
const api = new PokemonClient();
try {
const pokemonData = await api.getPokemonByName(slug);
console.log(pokemonData.name);
return pokemonData;
} catch (error) {
console.error(error);
return null;
}
}
);
- And then let’s get it from the Page component:
const Page: NextPage<PageProps> = async ({ params }) => {
const pokemon = await getData(params?.slug);
return <p>Pokemon: {pokemon?.name || "Not found"}</p>;
};
app/pokemons/[slug]/page.tsx
If you go to /pokemons/raichu
you should see the name from the PokeAPI 🎉.
Front-end
Now that we have everything set up, let’s plan on what to do next:
- Create a PLP (Pokemon Listing Page) and a PDP (Pokemon Detail Page).
- Style the whole website and make it look like the old games, it’d be great to recreate a pixel-style CSS.
- Create a series of Components that we can re-use while implementing.
I’ll use AI to give me a template to follow, and maybe even some tailwind class-set to re-create it, starting with a nice pixel-style banner. I will implement it and style it. It takes a while creating different components with different styles. For now I just want to focus on the experience, so I built a homepage and a PDP.
As soon as I finished building the PDP, I felt like it wasn’t aligned enough to the Pokémon experience. Remember that one of the benefits of using Commerce Layer is that I can create an user experience that isn’t rigid and bounded by what the commerce engine provides.
I was thinking that I might create a "launch the pokéball" to add the Pokémon to the cart. And while this might be a bit of an ambitious plan, let’s try it.
And it’s starting to look a lot like the game. I do want to add moves and evolutions below the catching screen, but that’s for another time. For now, I’ll stick with the Pokéball animation. Then I’ll move onto Commerce Layer integration.
💡 If you want to start the project now, you can checkout at the step-1
tag!
Commerce Layer
Now that I have a PDP, it’s time for me to connect to Commerce Layer. First of all, I need to set some data in CL.
- Create a Merchant.
- Add a US Market.
- Add some pokemon SKUs.
- Add prices for those SKUs in the US Market.
I won’t need a stock location or an inventory model as it’s all virtual. (However, I will add those later to test some logic.)
Next up, I will expose this API by creating a sales channel, which I then need to connect to my store.
Connect the Next.js app
I want to create a CommerceLayerAuth
provider that will be responsible of getting and refreshing the token to pass to the CommerceLayer
component.
To do so, I first need to install a couple npm packages:
pnpm install @commercelayer/react-components @commercelayer/js-auth
Let’s start this component from its props:
export type CommerceLayerAuthProps = {
children: React.ReactNode; // This will be the wrapped application.
clientId: string; // Ultimately will be coming from env variables (apply to the next two as well).
market: number;
slug: string;
}
Let’s then write the logic around getting the authorization (and store it in a state).
const CommerceLayerAuth = ({
children,
clientId,
market,
}: CommerceLayerAuthProps) => {
const [authorization, setAuthorization] =
useState<AuthenticateReturn<"client_credentials"> | null>(null);
useEffect(() => {
// @todo: get authorization from local storage if it exists.
const fetchAuthorization = async () => {
const auth = await authenticate("client_credentials", {
clientId,
scope: `market:code:${market}`,
});
setAuthorization(auth);
};
fetchAuthorization();
}, [clientId, market]);
return (
<div>
<code>{JSON.stringify(authorization)}</code>
<p>{children}</p>
</div>
);
};
And then I can save (and check) using the local storage for the authorization token.
// @note: I've created these three helpers.
type Authorization = AuthenticateReturn<"client_credentials">;
const getStoredAuthorization = (market: number): Authorization | null => {
const authAsString = localStorage.getItem(`authorization-${market}`);
if (authAsString != null) {
const localAuth = JSON.parse(authAsString);
return {
...localAuth,
expires: new Date(localAuth.expires),
};
}
return null;
};
const hasExpired = (time?: Date): boolean => {
if (!time) {
return true;
}
return time < new Date(Date.now());
};
const isValid = (auth: Authorization | null): auth is Authorization => {
if (!auth) {
return false;
}
return !hasExpired(auth.expires);
};
// @note: And inside the useEffect:
useEffect(() => {
const storedAuthorization = getStoredAuthorization(market);
if (isValid(storedAuthorization)) {
setAuthorization(storedAuthorization);
return;
}
// authenticate and then save in the local storage using the market:
localStorage.setItem(`authorization-${market}`, JSON.stringify(auth));
Now, I simply need to pass everything to the CommerceLayer provider, add a OrderStorage, and wrap the whole app with that.
return (
<CommerceLayer
accessToken={authorization.accessToken}
endpoint={`https://${slug}.commercelayer.io`}
>
<OrderStorage persistKey={`order-${market}`}>
{/* @ts-expect-error: OrderContainer children do not align to ReactNode type. */}
<OrderContainer>{children}</OrderContainer>
</OrderStorage>
</CommerceLayer>
);
Load data from Commerce Layer
The first thing I want to try is to get the computed price from my Auth → Market → Price List → SKU. To do so, I will simply use the Price
component from the CL React Component library).
I pass the sku
to the PokemonCard
, which has now:
<PricesContainer>
<Price skuCode={sku} />
</PricesContainer>
And when I refresh my homepage and PDP with the Grid, I get all the prices:
Add to Cart
Now that I can check the price coming from Commerce Layer, I want to connect the “CATCH IT” with adding to the cart. To do that, I’ll need to use the SDK via the useOrderContainer
hook as the react component won’t work in this case.
It’s quite simple:
// import from the react-components library.
import { useOrderContainer } from "@commercelayer/react-components";
// get the `addToCart` function from the hook.
const { addToCart } = useOrderContainer();
// use it when the catchingStatus is "catching"
useEffect(() => {
if (catchingStatus === "catching") {
addToCart({
skuCode: pokemon.name,
quantity: 1,
}).then(() => {
setCatchingStatus("cought");
});
}
}, [addToCart, catchingStatus, pokemon]);
Cart quantity
Before I test the add to cart, I want to integrate the Basket
component, so that I can see if it works. I want the trolley to have a pokéball in when something is added to the order. I don’t really care about the number at this point.
Since my Basket
components accepts an items: number
property, I will add the logic to the Header
.
I will use the hook again to get information about the order.
const { order } = useOrderContainer();
<Basket items={order?.skus_count} />
Inside the Basket component, I will use the CartLink component from Commerce Layer.
import { CartLink } from "@commercelayer/react-components";
import { useMemo } from "react";
export type BasketProps = {
items?: number;
};
const Basket = ({ items = 0 }: BasketProps) => {
const imageSource = useMemo(() => {
return items === 0 ? "/images/basket-empty.png" : "/images/basket-full.png";
}, [items]);
return (
<CartLink
className="w-7 inline-block"
label={<img src={imageSource} className="w-full" alt="Basket" />}
/>
);
};
export default Basket;
Let’s test if our “Catch to Cart” works.
YAY 🎉!
Push the code and deploy
Now I’m ready to push and deploy to Vercel. Don’t forget to add your env variables!
The repository can be find here.
Conclusion
To recap, we've seamlessly integrated product data from an external API with Commerce Layer's robust commerce capabilities, creating a unique shopping experience. We've shown not only how to integrate an external API, but also that you can bring innovative ideas to some of the most sterile experiences like cart and checkout.
The highel-level key takeaway?
Commerce Layer empowers you to sell anything, anywhere, without limitations. Whether you're dealing with virtual goods or physical products, Commerce Layer provides the flexibility and tools to bring your innovative commerce ideas to life quickly and efficiently. From rapid prototyping to scaling complex operations, Commerce Layer adapts to your needs, making it the ideal choice for businesses looking to push the boundaries of commerce.