Eflintdev

Page Content Section

Storefront Frontend Series: MERN Stack, GraphQL, and AWS ECS - Part 1

Building Modern Storefronts with Figma, Tailwind, MERN and more...
Storefront frontend series feature - part 1

Building frontend assets for online stores, with Figma, Tailwind, Next.js, MERN stack, GraphQL, and more...

This articles delves into several of the important front-end and fullstack development technologies that greatly benefit modern online storefront page development.

The Details

As a developer who has worked in a good amount of application development lifecycles or sprints involving the creation of landing pages, I’ve come to appreciate the need for speed. Landing pages can be resource-intensive because they often comprise large amounts of data. Moreover, much of this data might come from multiple endpoints or domains.

This tends to necessiate making a few or more API calls to several endpoints when designing the data flow for a landing page. Such obviously involves some strategic planning concerning the page layout, components, state management, information architecture, and data provisioning.

Starting with The Design

One of the more common or recurring design motifs for modern online storefronts seems to be the illustrative product card. These collectively give users a comprehensive indication of what an online e-commerce venue has to offer, in a very efficient and user-friendly manner. This is probably why streaming services use cards as the means for keeping their users’ invested in watching. Most developers are very used to dealing with these cards, even as users. So, as a front-end engineer, I’m always enthused about working with card components.

Breaking Ground with Figma

Here are some Figma wireframes for cards that I’ve developed. Yes, we have three designs which could be found in different kinds of online storefronts:

Product Card Examples

  • Online Course Product Card
  • Finance Digital Asset Card
  • Online Film Streaming Feature Card
figma wireframes for online storefront cards screenshot
figma wireframes for online storefront cards screenshot

For the purpose of this article, I selected the online learning course product card and created a pixel-perfect replica of it in plain HTML and CSS. Yes, the ‘vanilla’ template is a good point of departure. Also, I didn’t want to restrict the use of this card to one frontend framework or library. Now, for the CSS styling I decided to use Tailwind CSS, which tends to produce very light stylesheets that certainly push the DRY (Don’t Repeat Yourself) concept, with robust utility class features.

<!-- Banner CTA Block STARTs here --> <div class="banner-cta__block bg-la-blue-400 w-[22.1875rem] p-6 rounded-lg md:mx-auto mb-[53px]"> <!-- Banner CTA Title STARTs here --> <h2 class="banner-cta__title text-white uppercase text-[1.75rem] leading-8 font-bold mb-4">Enjoy Huge Savings This Month!</h2> <!-- Banner CTA Title ENDs here --> <!-- Banner CTA Text Content STARTs here --> <p class="banner-cta__text__content text-white body-larger-semibold mb-4">Save big when learning online, by taking advantage of the amazing discounts and promotions!</p> <!-- Banner CTA Text Content ENDs here --> <!-- Banner CTA Link STARTs here --> <a class="banner-cta__link button-hollow--base cta-button--hollow-wht-to-blue-400" href="/">Show Now</a> <!-- Banner CTA Link ENDs here --> </div> <!-- Banner CTA Block ENDs here -->

Tailwind Makes a Lean Stylesheet

Here is an example of a simple storefront CTA component that I created for a banner, using Tailwind utility classes. The great thing about this component is that it has NO STYLESHEET module. All the color, spacing, layout, and any breaking point details have been specified with Tailwind utility classes. However, keep in mind that without a specific tailwind.config.js file, the HTML template markup and CSS styles might not render the same in a different project that implements Tailwind.

screenshot of tailwind cta component

Rendered CTA Result

So, here is the rendered result (without having to create a stylesheet). Tailwind sorts out all the utilized classes to generate an optimized set of styles that don't repeat. The great part about these utility classes is that they can be fine-tuned for more accuracy. For example, the font size as text-[1.75rem], or margin-bottom as mb-[53px].

figma online course card wireframe

Back to the retail cards...

There is one thing about working with these cards. The body or content portions usually require the most revision. In React and Next.js, we have the HOC (Higher Order Component) paradigm which when coupled with the Composition Component concept presents a highly efficient strategy for pushing a component’s reusability. In other frameworks the equivalent would probably be a component factory of some kind.

Let's examine the online course card's apparent structure:

Product Image Container

  • Product Image
  • Product Rank Indication (best seller, etc.)
  • Wishlist Button and Indicator
  • Product Type Indicator
  • Product Ribbon (text ribbon for extra descriptions or slogans)
  • Product Title or Name Block
  • Course Credits or Credentials (to be earned)
  • Course Scheduling
  • Pricing Block
  • Ratings Block

To create very a useful online learning course card, I needed to include specific information that would appeal to the potential users. So, as part of the UX research for this interesting front-end R&D series, I visited these online course providers' sites.

Online Learning Providers

  • CFTEA
  • Coursera
  • edX
  • LinkedIn Learning
  • MindEdge
  • Pluralsight
  • Udemy

It's Atomic

So, based on what I observed about commercial trends, a well-rounded online learning course card needs to provide some essential information, so that users can make informed decisions while navigating a site, making searches, comparing, contrasting, etc. Also, to ensure that a product card will be as compact as possible some details would be assembled in some form of a popup. For this project, I decided to put the course credits in a pop-up that would be designed as a React Higher Order Component (HOC).

'use client'; import React, { useState } from 'react'; import { JSX } from '@emotion/react/jsx-runtime'; ... export const ProductCardComponent = ({ data }: { data: Course | Certificate }): JSX.Element => { const { credits, details, image, ratings, schedule, title, type } = data; const { regularPrice, strikeThroughPrice, rank, ribbonShade, ribbonText, typeLabel, typeLabelPosX } = details; /** Wishlisted toggle state */ const [isWishListed, setIsWishListed] = useState<boolean>(false); /** Best Seller toggle state */ const [isBestSeller, _setIsBestSeller] = useState<boolean>(details.rank === IS_BEST_SELLER); /** Best Value toggle state */ const [isBestValue, _setIsBestValue] = useState<boolean>(details.rank === IS_BEST_VALUE); /** Certificate toggle state */ const [isCertificate, _setIsCertificate] = useState<boolean>(type === CERTIFICATE); /** * Product card base and toggle classes utility. */ const classes = classNames( PRODUCT_CARD_CLASSES.base, PRODUCT_CARD_CLASSES.toggle(isWishListed, isBestSeller, isBestValue, isCertificate) ); /** * Toggles the wishlisted or favorite indicator. */ const toggleWishlisted = (): void => { setIsWishListed(previousState => !previousState); } /** HOC for Tooltip with credits list. */ const HocCreditsListToolTip = withTooltipCreditsList(CreditsListComponent, { ...credits, itemClassName: '' }); const cardImageProps = { image, rank, ribbonText, ribbonShade, toggleWishlisted, typeLabel, typeLabelPosX }; return ( <React.Fragment> {/* Card Wrapper STARTs here */} <div className={classes}> {/* Card Body STARTs here */} <div className="card-body"> <CardImageComponent data={cardImageProps} /> {/* Card Content Block STARTs here */} <a className="block card-content__block pt-4 px-4 pb-5" href="/"> <CardTitleComponent data={title} /> <HocCreditsListToolTip /> <MetricsComponent data={schedule} /> <ProductPricingComponent data={{ regularPrice, strikeThroughPrice }} /> <RatingComponent data={ratings} /> </a> {/* Card Content Block ENDs here */} </div> {/* Card Body ENDs here */} </div> {/* Card Wrapper ENDs here */} </React.Fragment> ); } export default ProductCardComponent;

Product Card React Component in Next.js

This is the code for a React component that I created in response to my insights about atomic strategy. It was developed inside a Next.js project.

/** * Course Base - the parent class for Course and Certificate */ export class CourseBase { public active!: boolean; public id!: number; public title!: string; public brandName!: string | null; public brandId!: number | null; public credits!: ProductCredits; public details!: ProductEcommerce; public ratings!: ProductRating; public schedule!: ProductSchedule; public image!: string | null; public description!: string | null; public descriptionHtml!: string | null; }

The Course Product Data Model

In consideration of the intentions regarding entity relationships, the data model was devised as such to allow for more flexibility when using GraphQL queries. This means that the credits, course schedule, course details, and ratings were planned to be brought in using Mongoose Refs (references).

https://somewebsite.com/articles/?fields[articles]=id,title,image,credits,details,ratings,schedule

Flexible Http Requests

Now given the data requirements for the course card it’s highly likely that certain fields would need to be trimmed off (hidden), so to speak. Sure, I could just hide those fields in the API, but doing that would require modifying another resource other than the frontend, which leads to more pull requests.

Another option would be selecting specific fields through JSON API Spec’s field selection feature. Here is an example of that approach in a Http request:

Apart from the pre-existing desire to implement GraphQL, I also understood that custom field selections would be easily achievable through querying. At this point, I took some time to further infuse my thought process with Agile Methodology, by defining “the story” for an established portion of what I’ve related regarding the data’s flexibility:

As a

Developer, administrator, or manager

I want

To easily and quickly adjust data fields or relational properties to frontend assets in a way that doesn’t require lengthy adjustments to other resources, such as shipped API codebases or databases.

So that I can

Intuitively customize the appearance of frontend collateral on a landing page (for example) in an online storefront.

Plus, I want the added possibility of receiving real-time updates to dashboard data that represents up-to-date product inventory, sales, or analytics.

Building a GraphQL API

When I started working on this project I was intent on working within a MERN stack (fullstack). For the card design I created I didn't need the description, descriptionHTML, and parentCerts fields from the database. If we multiply the total size of these three data properties’ values by the number of cards that will appear on the landing page (or in the entire site), we essentially have a considerable number of kilobytes (or more) that can be trimmed off the request response(s) – equaling some significant performance optimization.

Since I’d be using MongoDB for a database it made sense to look for a NodeJS framework that could be used to implement a GraphQL API – one that also leverages Mongoose (ORM). So, after conducting further research I settled on developing the learning course API with NestJS.

The "E" in MERN would be provided through Apollo Server Integration for Express V5 (@as-integrations/express5)

NestJS Framework

Nest JS allows developers to define schema by using models (classes) that are augmented with annotations or decorators. Apart from the schema we also need the input types, DTOs (Data Transfer Objects), services, repositories, resolvers, and controllers. These constitute a set of assets that make it possible to make requests with standard routes, or alternately with GraphQL queries.

Ensuring Proper Data Structure(s) with Mongoose

The data for this project comprises specific structures that are modeled with Mongoose Schemas that allow for specifying TYPE validation parameters. Note how the Schema, Data Transfer Object, and InputType are base classes (in the following code example), that were created to minimize any possible repetition in later uses. Such could be an undoubtedly similar CreateCourseDto, which would require various changes to its strictness on a few properties.

In the first code example below we have CourseBase Schema, which defines the Courses Collection. The following code example presents the CourseBaseDTO (Data Transfer Object) class and the CourseBaseInput class for validation below it:

/** * (NestJS Mongoose) Schema serves as the basis for courses and certificates table/collection. */ @Schema({ discriminatorKey: 'type' }) export class CourseBase extends Document { @Prop({ type: Number, unique: true, sparse: true }) // required set up for auto-incrementing trigger id!: number; @Prop({ required: true, default: false }) active!: boolean; @Prop({ type: Number }) brandId!: number; @Prop({ type: String }) brandName!: string; @Prop({ type: String, required: true, unique: true }) title!: string; @Prop({ type: String }) image!: string; @Prop({ type: String, required: true }) description!: string | null; @Prop({ type: String, required: true }) descriptionHtml!: string | null; // Relationships @ValidateNested({ each: true }) @Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Credit', default: null, }) credits!: Credit; @ValidateNested({ each: true }) @Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Rating', default: null, }) ratings!: Rating; @ValidateNested({ each: true }) @Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Detail', default: null, }) details!: Detail; @ValidateNested({ each: true }) @Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Schedule', default: null, }) schedule!: Schedule; } export type CourseBaseEntityDocument = CourseBase & Document; export const CourseBaseSchema = SchemaFactory.createForClass(CourseBase); CourseBaseSchema.plugin(mongoosePaginate.default);
// course-base.dto.ts /** * (NestJS Mongoose) Course Base Data Transfer Object */ @ObjectType() export class CourseBaseDto { @Field(() => Number) id?: number; @IsBoolean() @Field(() => Boolean) active?: boolean; @IsOptional() @IsNumber() @Field(() => Number, { nullable: true }) brandId?: number | null; @IsOptional() @IsString() @Field(() => String, { nullable: true }) brandName?: string | null; @IsOptional() @IsString() @Field(() => String, { nullable: true }) title?: string | null; @IsOptional() @IsString() @Field(() => String, { nullable: true }) image?: string | null; @IsOptional() @IsString() @Field(() => String, { nullable: true }) description?: string | null; @IsOptional() @IsString() @Field(() => String, { nullable: true }) descriptionHtml?: string | null; // Relationships @IsOptional() @Field(() => CreditDto, { nullable: true }) credits?: CreditDto; @IsOptional() @Field(() => RatingDto, { nullable: true }) ratings?: RatingDto; @IsOptional() @Field(() => DetailDto, { nullable: true }) details?: DetailDto; @IsOptional() @Field(() => ScheduleDto, { nullable: true }) schedule?: ScheduleDto; } // course-base.input.ts /** * Course Base Input Validation Type */ @InputType() export class CourseBaseInput { @IsOptional() @IsInt() @Field(() => Int, { nullable: true}) courseId?: number | null; @IsOptional() @IsBoolean() @Field(() => Boolean, { nullable: true }) active?: boolean; @IsOptional() @IsNumber() @Field(() => Number, { nullable: true }) brandId?: number | null; @IsOptional() @IsString() @Field(() => ProductType, { nullable: true }) type?: string; @IsOptional() @IsString() @Field(() => String, { nullable: true }) brandName?: string | null; @IsOptional() @IsString() @Field(() => String, { nullable: true }) title?: string | null; @IsOptional() @IsString() @Field(() => String, { nullable: true }) image?: string | null; @IsOptional() @IsString() @Field(() => String, { nullable: true }) description?: string | null; @IsOptional() @IsString() @Field(() => String, { nullable: true }) descriptionHtml?: string | null; // Relationships @IsOptional() @Field(() => CreditInput, { nullable: true }) credits?: CreditInput; @IsOptional() @Field(() => RatingInput, { nullable: true }) ratings?: RatingInput; @IsOptional() @Field(() => DetailInput, { nullable: true }) details?: DetailInput; @IsOptional() @Field(() => ScheduleInput, { nullable: true }) schedule?: ScheduleInput; }

Entity Relationship Diagrams

During the data modeling process it was necessary to continually examine and refine a set of ER diagrams.

Course Entity Relationship Diagram
Certificate Entity Relationship Diagram
Entity Relationship Many-to-Many diagram - Courses and Certificates
Lucid Chart UI screenshot

Optimizing the Response with GraphQL

As mentioned before, optimizing the response is crucial for cost-control and adherence to service quotas. Notice how the descriptions, and active properties were excluded from the GraphQL query. Now, have a look at the following code example which shows what the response for this query looks like, after being made in Postman.

// NestJS GraphQL Query for findAllCourses w/ pagination query GetCourses { findAllCourses(paginationInput: { page: 1, limit: 50, populate: ["credits","details","ratings","schedule"]}){ data { id courseId brandId brandName title type image credits { id ceu shrm atd pdu hrci } details { id regularPrice strikeThroughPrice isCertificate rank ribbonText ribbonShade typeLabel typeLabelPosX } schedule { accessTime duration } ratings { totalNumReviews value } } page limit total totalPages } }
// NestJS GraphQL response for findAllCourses w/ pagination { "data": { "findAllCourses": { "data": [ { "_id": "69228a4e34ae14a5d2146b78", "id": 37, "courseId": 37, "brandId": 11, "brandName": "AlumniGo Magazine", "title": "Managing Global Agile Scrum Teams", "type": "course", "image": "images/product-images/female-professional-worker-at-desk-460x322.webp", "credits": { "id": 32, "ceu": "3.0", "shrm": "5.5", "atd": "4.4", "pdu": "11", "hrci": "34" }, "details": { "id": 14, "regularPrice": "159.00", "strikeThroughPrice": "259.00", "isCertificate": false, "rank": "isBestSeller", "ribbonText": "Pro Series", "ribbonShade": "blue", "typeLabel": null, "typeLabelPosX": null }, "schedule": { "id": 27, "accessTime": "180", "duration": "36" }, "ratings": { "id": 28, "totalNumReviews": 1134, "value": "3.4" } } ... ], "page": 1, "limit": 50, "total": 6, "totalPages": 1 } } }

Swagger API Output and GraphQL Query Tests

1.) Another great thing about NestJS is that it can be setup to output Swagger documentation. You can view my current setup using this video link: Learning Course API Swagger Documentation

2.) This is a video capture of a GraphQL mutation test, I did in Postman for creating a course in the MongoDB courses collection: GraphQL Mutation Test.

3.) This is another video of a mutation test but with more detail. It focuses on field selection, along with related entity selection capabilities: Detailed GraphQL Mutation Test.

# Next.js App Folder Structure . ├── about │ └── page.tsx ├── brands │ ├── [brandId] │ │ └── [slug] │ │ └── page.tsx │ └── page.tsx ├── cart │ └── page.tsx ├── categories │ ├── [categoryId] │ │ ├── page.tsx │ │ └── [slug] │ │ └── page.tsx │ └── page.tsx ├── contact │ └── page.tsx ├── global.scss ├── layout.tsx ├── page.tsx └── StoreProvider.tsx

Next.js Folder Structure

Since the data requirements were essentially met for the part of the app I was focusing on, I decided to spend some more time in Next.js. The first task was to build out the folder structure, which directly correlated with the routing. I developed the StoreProvider and slices that would be required for working with Redux and controlling the various states in the application.

services: app: # Next.js App container_name: learningstore_app build: ./apps/frontend/. image: learningstore-frontend:latest networks: - apps_network ports: # Port mapping as <client/ui port>:<container port> - 3000:3000 expose: - 3000 depends_on: - api api: # Nest JS API container_name: learningstore_api build: ./apps/api/. image: learningstore-api:latest networks: - apps_network ports: # Port mapping as <client/ui port>:<container port> - 5000:5000 expose: - 5000 environment: # Mapping environment variables from .env file to container environment vars - PORT=${PORT} - MONGODB_URI=${MONGODB_URI} - MONGODB_DATABASE=${MONGODB_DATABASE} depends_on: - db db: # MongoDB Remote Connection container_name: mongodb image: mongo:latest restart: always # Mapping the container environment variables into database vars environment: MONGODB_INITDB_ROOT_USERNAME: ${MONGODB_USER} MONGODB_INITDB_ROOT_PASSWORD: ${MONGODB_PASSWORD} MONGODB_INITDB_DATABASE: ${MONGODB_DATABASE} ports: - 27017:27017 networks: apps_network: driver: bridge

What's Next?

In the upcoming Part 2 for this article, I'll be showing more of the frontend architecture setup in the Next.js portion of this project. In React/NextJS I'll specifically be setting up Redux and Thunk, along with consuming the data from the API via Apollo Client to infuse various components with data. Afterwards, there will be some additional work with Docker Compose along with the eventual test deployments to AWS Elastic Container Registry and AWS Elastic Container Service. Have a peek at the docker-compose.yml I've composed in the following code snippet, which I'll be working with in the subsequent phases. These will involve the security (authorization, authentication, guards, and token validation) for the GraphQL API - possibly in Part 3 for this series.

Kind regards,

Emmanuel C. Flint

Frontend Engineer

November 25, 2025

References

Meta Platforms, Inc. (2025). Built-in React Hooks. Next.js. https://react.dev/reference/react/hooks.

Meta Platforms, Inc. (2025). useState. Next.js. https://react.dev/reference/react/useState.

Mysliwiec, K. (n.d.). Mongo. NestJS. https://docs.nestjs.com/techniques/mongodb.

Okeh, O. (2022, June 20). Implementing pagination with GraphQL in NestJS. LogRocket. https://blog.logrocket.com/implementing-pagination-graphql-nestjs.

The GraphQL Foundation. (2025, November 16). Mutations: Learn how to modify data with a GraphQL server. GraphQL. https://graphql.org/learn/mutations.

Vercel, Inc. (2025). Project structure and organization. Next.js. https://nextjs.org/docs/app/getting-started/project-structure.

Loader