How I Built an E-commerce Platform like Shopify in 3 Weekends with AWS Amplify

When Ambition meets AWS Amplify

How I Built an E-commerce Platform like Shopify in 3 Weekends with AWS Amplify

Before we dive into the nitty-gritty details of how I built this Shopify alternative app using AWS Amplify, I want to apologize for the slightly clickbaity title. While the core facts are true - I did build this app in roughly 70 hours over two weeks - I recognize that headlines like "How I Built X in Y Time" can come across as exaggerated or misleading.

My intention with the title was simply to convey the ambitious yet achievable nature of this project. With the right technology stack and approach, even complex web applications can be built surprisingly quickly these days. But I don't want you to walk away thinking I somehow hacked time or defied the laws of software development! The title is meant to pique your interest and get you curious about the actual story, not mislead you in any way.

So with that out of the way, let's dive into the real nitty gritty. I'll walk you through the problem I set out to solve, the technology choices I made, the challenges I faced, and - of course - how I built this Shopify alternative app in just three crazy weekends. I hope my story inspires you to tackle your ambitious weekend projects, armed with the right tools and strategies to succeed.


Introduction

If you've ever dreamed of building a full-stack app but have been intimidated by the technical challenges of setting up the whole backend, this story is for you.

Using AWS Amplify, I quickly generated the frontend components I needed - from forms and tables to modals and menus. Amplify Studio's drag-and-drop interface allowed me to build the app's data models and APIs in just a few hours.

Behind the scenes, Amplify is handling all the backend work for me - hosting my app, managing the database, and integrating features like analytics and cloud storage. This freed me up to focus on the user experience and functionality that matters most for the users.

The result? An all-in-one e-commerce platform that helps users:

• Create and manage their product catalog, customers, and orders.
• Store product images and documents in the cloud
• Send targeted promotional emails to the right customers
• Track sales, orders, and other key metrics

In the following article, I'll walk you through how I built this app in just 3 crazy weekends. I hope that by sharing my story and lessons learned, I'll inspire you to tackle your ambitious weekend projects.


Before moving forward, Let's see the features of the app

  1. 🔐 Secure Login/Signup with AWS Cognito - Amplify makes it a breeze to integrate user authentication and authorization using AWS Cognito. Users can quickly create accounts and log in to access the app's features.

  2. 📝 GraphQL API - Amplify provides a GraphQL API out of the box to interact with the front end. This allowed me to quickly build the data models and CRUD operations for products, orders, and customers.

  3. ⚛️ Amplify UI Components - I leveraged Amplify's React UI library to generate forms, tables, modal windows, and other UI components - saving me hours of frontend development work.

  4. 🎨 Amplify Studio Data Models - Amplify Studio's visual interface helped me design and generate the data models for my backend, which I then connected to my React frontend.

  5. 📧 AWS SES + Pinpoint Email Campaigns - I integrated AWS SES and Pinpoint to enable targeted promotional email campaigns to the right customers at the right time.

  6. 📊 Built-in Analytics with Pinpoint - Amplify's integration with Pinpoint gave me real-time analytics and metrics like total auto-tracking a page time spent, auto-tracking user sessions, and page views - right out of the box.

🛠 Check out the code and demo:

💻 GitHub Repo: https://github.com/vishesh-baghel/amplify-hackathon

🖥 Live Demo: https://main.d324liwg52d96r.amplifyapp.com/

Login credentials- email: , password: Test@123


Let's get into the details

Routes configured in the app

function MyRoutes() {
  const { route } = useAuthenticator((context) => [context.route]);
  Storage.configure({ track: true });

  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route
            index
            element={route === "authenticated" ? <Dashboard /> : <Home />}
          />
          <Route
            path="/cart"
            element={
              <RequireAuth>
                <Cart />
              </RequireAuth>
            }
          />
          <Route path="/collections" element={<Collections />} />
          <Route
            path="/products"
            element={
              <RequireAuth>
                <ProductSummary />
              </RequireAuth>
            }
          />
          <Route
            path="/customers"
            element={
              <RequireAuth>
                <CustomerSummary />
              </RequireAuth>
            }
          />
          <Route
            path="/orders"
            element={
              <RequireAuth>
                <OrderSummary />
              </RequireAuth>
            }
          />
          <Route
            path="/storage"
            element={
              <RequireAuth>
                <StorageSummary />
              </RequireAuth>
            }
          />
          <Route
            path="/marketing"
            element={
              <RequireAuth>
                <Marketing />
              </RequireAuth>
            }
          />
          <Route
            path="/analytics"
            element={
              <RequireAuth>
                <Analytics />
              </RequireAuth>
            }
          />
          <Route
            path="/profile"
            element={
              <RequireAuth>
                <Profile />
              </RequireAuth>
            }
          />
          <Route path="/login" element={<Login />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function App() {
  return (
    <Authenticator.Provider>
      <MyRoutes />
    </Authenticator.Provider>
  );
}

export default withInAppMessaging(App);

we will get into what withInAppMessaging(App) is, in the upcoming sections.

App's Layout

export function Layout() {
  useEffect(() => {
    Analytics.autoTrack("event", {
      enable: true,
      events: ["click"],
      selectorPrefix: "data-amplify-analytics-name",
      provider: "AWSPinpoint",
    });
  }, []);

  const { route } = useAuthenticator((context) => [
    context.route,
    context.signOut,
  ]);

  const [userProfilePhoto, setUserProfilePhoto] = React.useState("");

  const navigate = useNavigate();

  React.useEffect(() => {
    getUserInfo();
  }, []);

  async function getUserInfo() {
    const user = await Auth.currentAuthenticatedUser();
    downloadFile(user);
  }

  const downloadFile = async (user) => {
    await Storage.get(`${user.attributes.picture}`, {
      level: "public",
    })
      .then((result) => {
        setUserProfilePhoto(result);
      })
      .catch((err) => console.log(err));
  };

  return (
    <View className={style.container}>
      {route !== "authenticated" ? (
        <NavBarHeader2
          width={"100%"}
          className={style.navbar}
          loginHandler={() => navigate("/login")}
          data-amplify-analytics-name="login"
        />
      ) : (
        <>
          <NavBarHeader
            width={"100%"}
            className={style.navbar}
            cartHandler={() => navigate("/cart")}
            profileImage={userProfilePhoto}
            data-amplify-analytics-name="cart"
          />
        </>
      )}
      <Outlet />
    </View>
  );
}

Let's see what this code this doing:

useEffect(() => {
    Analytics.autoTrack("event", {
      enable: true,
      events: ["click"],
      selectorPrefix: "data-amplify-analytics-name",
      provider: "AWSPinpoint",
    });
  }, []);

Inside the component, there is a useEffect hook that enables auto-event tracking using Amplify's Analytics API. It sets up the tracking to capture click events on elements with the data-amplify-analytics-name attribute and uses AWS Pinpoint as the analytics provider. See the Amplify docs here for more details on how the analytics works in Amplify.

Models Generated via Amplify Studio

type AuditLogs @model @auth(rules: [{allow: public}]) {
  id: ID!
  model: String
  opType: String
}

type Cart @model @auth(rules: [{allow: public}]) {
  id: ID!
  createdAt: AWSTimestamp
  updatedAt: AWSTimestamp
  Products: [Product] @hasMany(indexName: "byCart", fields: ["id"])
}

type Inventory @model @auth(rules: [{allow: public}]) {
  id: ID!
  quantity: Int
  location: Address
  lastUpdated: AWSTimestamp
  productID: ID! @index(name: "byProduct")
}

type Product @model @auth(rules: [{allow: public}]) {
  id: ID!
  name: String
  description: String
  price: String
  category: String
  productTags: [String]
  Inventories: [Inventory] @hasMany(indexName: "byProduct", fields: ["id"])
  cartID: ID @index(name: "byCart")
  productImages: [String]
}

type OrderItem {
  id: ID
  quantity: Int
  price: Float
  productID: ID
}

type Order @model @auth(rules: [{allow: public}]) {
  id: ID!
  shippingAddress: Address
  billingAddress: Address
  totalAmount: Float
  status: String
  items: [OrderItem]
  customerID: ID! @index(name: "byCustomer")
}

type Address {
  id: ID
  recipientName: String
  street: String
  city: String
  state: String
  postalCode: String
  country: String
}

type Customer @model @auth(rules: [{allow: public}]) {
  id: ID!
  name: String!
  dateOfBirth: AWSDate
  email: AWSEmail
  billingAddress: Address
  shippingAddress: [Address]
  profileImage: String
  gender: String
  Orders: [Order] @hasMany(indexName: "byCustomer", fields: ["id"])
  Cart: Cart @hasOne
}

One interesting thing to notice here is AuditLogs. I am using DataStore.observe() method to listen to all the changes happening in any specific model. For example, I am listening to all the changes happening in the Customer model in the below code:

useEffect(() => {
    const customerSub = DataStore.observe(Customer).subscribe((msg) => {
      DataStore.save(
        new AuditLogs({
          model: "Customer",
          opType: msg.opType.toString(),
        })
      )
        .then(() => console.log("saved customer"))
        .catch((err) => console.log(err));
    });

    return () => {
      customerSub.unsubscribe();
    };
  });

To know more about DataStore, see these docs from Amplify.

You can easily create or edit your models as per your requirements using the amplify studio visual editor for models. For example, the customer model for this app looks something like this in the console.

you can easily add more fields to the model. Also, you can create relationships and associations with other models and types to make your database more sophisticated.

How authentication works in the app

export function Login() {
  const { route } = useAuthenticator((context) => [context.route]);
  const location = useLocation();
  const navigate = useNavigate();
  let from = location.state?.from?.pathname || "/";

  useEffect(() => {
    if (route === "authenticated") {
      navigate(from, { replace: true });
    }
  }, [route, navigate, from]);

  return (
    <View className="auth-wrapper">
      <Authenticator></Authenticator>
    </View>
  );
}

All you have to do is add this login component and set up a route for that in your app and Amplify will take care of everything related to authentication. From login to signup, everything is out-of-the-box just like magic. Your login page will look something like this-

you can also configure different fields(like phone number, gender, address, etc.) you want to include at the time of account creation. See these docs.

My sign-up forms look something like this-

As you can see. I have added a custom attribute name in the sign-up form.

Add a Storage feature in your app using StorageManager from Amplify UI library

<StorageManager acceptedFileTypes={["image/png", "video/*",]}
    accessLevel="public"
    maxFileCount={5}
    maxFileSize={10000000}
    isResumable
    processFile={processFile}
/>

const processFile = async ({ file }) => {
    const fileExtension = file.name.split(".").pop();
    return file
      .arrayBuffer()
      .then((filebuffer) => window.crypto.subtle.digest("SHA-1", filebuffer))
      .then((hashBuffer) => {
        const hashArray = Array.from(new Uint8Array(hashBuffer));
        const hashHex = hashArray
          .map((a) => a.toString(16).padStart(2, "0"))
          .join("");
        return { file, key: `${hashHex}.${fileExtension}` };
      });
  };

Here, the processFile the function will generate a unique key for all your files uploaded to the aws S3 bucket.

This component will render the file uploader which looks like this-

I have wrapped the StorageManager component using Card component for adding details like the file size and accepted extensions. You can also add custom CSS and other props to StorageManager. Visit these docs for more details.

Track user actions using AWS Pinpoint API

  const [startTime, setStartTime] = useState(null);
  const [endTime, setEndTime] = useState(null);

  useEffect(() => {
    setStartTime(new Date());
    return () => setEndTime(new Date());
  }, []);

  useEffect(() => {
    if (startTime && endTime) {
      const seconds = (endTime.getTime() - startTime.getTime()) / 1000;
      Analytics.record({
        name: "timeOnCollectionPage",
        attributes: { timeOnPage: seconds },
      });
    }

    Analytics.autoTrack("session", {
      enable: true,
      attributes: {
        page: "Dashboard",
      },
    });

    Analytics.autoTrack("pageView", {
      enable: true,
      eventName: "DashboardPageView",
      type: "singlePageApp",
      provider: "AWSPinpoint",
      getUrl: () => {
        return window.location.origin + window.location.pathname;
      },
    });

    Analytics.autoTrack("event", {
      enable: true,
      events: ["click"],
      selectorPrefix: "data-amplify-analytics-name",
      provider: "AWSPinpoint",
    });
  }, [endTime]);

Here, I have used functions like Analytics.record and Analytics.autoTrack to track users with various parameters like sessions, pageview, events, and custom attributes like time spent on the page. See these docs for more details.

By integrating different analytics in the app, your pinpoint console will generate some analytics like this-

As you can see, we can track users on various parameters like daily or monthly active users.

Deploy your app automatically by just adding your git repository to the amplify console

  1. Sign in to the AWS Management Console and open the Amplify service.

  2. In the Amplify Console, click on the "Connect app" button.

  3. Select your Git provider (e.g., GitHub, Bitbucket, GitLab) and follow the prompts to authorize Amplify's access to your repository.

  4. Choose the repository and branch you want to deploy from.

  5. Configure the build settings for your app. This includes selecting the framework or platform, specifying the build commands, and setting environment variables if needed.

  6. Review the build settings and click on the "Next" button.

  7. Configure the deployment settings. This includes specifying the branch to deploy from, the build settings, and the environment variables.

  8. Review the deployment settings and click on the "Next" button.

  9. Optionally, you can set up a custom domain for your app by following the instructions provided. This step is not mandatory.

  10. Review the final settings and click on the "Save and Deploy" button.

After doing all these steps. your amplify project will look like this-

How to setup In-App messaging using AWS Pinpoint

This is the code that is present in the app dashboard:

const { InAppMessaging } = Notifications;
  const [sendInAppMessage, setSendInAppMessage] = React.useState(false);

  useEffect(() => {
    Analytics.record(refreshEvent);
    InAppMessaging.dispatchEvent(refreshEvent);
  }, [sendInAppMessage]);

  const refreshEvent = {
    name: "refresh",
    label: "Refresh",
    page: "Dashboard",
    handler: () => {
      console.log("refresh");
    },
  };

  useEffect(() => {
    InAppMessaging.syncMessages();
  }, []);

  useEffect(() => {
    const listener = InAppMessaging.onMessageReceived(myMessageReceivedHandler);
    return () => listener.remove();
  }, []);

  useEffect(() => {
    const displayListener = InAppMessaging.onMessageDisplayed(
      myMessageDisplayedHandler
    );
    return () => displayListener.remove();
  }, []);

  const myMessageReceivedHandler = (message) => {
    console.log(message);
  };

  const myMessageDisplayedHandler = (message) => {
    console.log("Message displayed", message);
  };

There are two ways of sending an in-app message using the notification service from Amplify.

  1. The first way is to record an event first using Analytics.record() and then calling the InAppMessaging.dispatch() function. It will trigger an event and then AWS pinpoint will send a message which is configured in your campaign. It is mandatory to keep a campaign running to listen to all the in-app-related messages.

  2. The second and simple way to create a campaign directly from the pinpoint console. The app had a campaign in the past, which used to trigger an in-app message notification on every login. You can see the campaign details below.

    In the schedule section, you can see the trigger event is set to _userauth_sign_in, which triggers an in-app message to the app.

    The users will see a message as seen in the screenshot. You can easily customize your message in the pinpoint campaign.


Things to keep in mind while working with Amplify

  1. Always do amplify push after making local changes: do this if you are adding something new or editing the amplify services by using amplify add or amplify update.

  2. Familiarize yourself with the Amplify documentation: AWS Amplify has extensive documentation that provides detailed information about its features, services, and best practices. Make sure to read the documentation thoroughly to understand how to use Amplify effectively.

  3. Understand the Amplify architecture: Amplify is built on top of various AWS services, such as AWS AppSync, AWS Lambda, and Amazon S3. It's crucial to have a good understanding of these underlying services to leverage Amplify's capabilities fully.

  4. Plan your project structure: Before starting with Amplify, it's essential to plan your project structure. Decide on the services you want to use, such as authentication, storage, or API, and organize your project accordingly. This will help you maintain a clean and scalable codebase.

  5. Use the Amplify CLI: The Amplify Command Line Interface (CLI) is a powerful tool that simplifies the process of setting up and managing your Amplify project. It allows you to create and configure services, deploy your application, and perform other essential tasks. Make sure to familiarize yourself with the Amplify CLI commands.


Conclusion

In this article, we explored the exciting journey of building a Shopify alternative app in just three weekends using AWS Amplify. We witnessed how Amplify's powerful features and integrations allowed us to rapidly develop an app. From the seamless login/signup process with AWS Cognito to the GraphQL API for efficient database manipulation, Amplify proved to be a game-changer.

We also delved into the Amplify UI library, which provided us with pre-built React components, saving us valuable development time. Amplify Studio further streamlined our backend tasks, enabling us to focus on delivering a seamless user experience.

Throughout this project, Amplify's integration with AWS services like S3 for storage and Pinpoint for analytics proved invaluable. We harnessed the power of AWS Amplify to build a robust, scalable, and user-friendly app.

Thanks for reading until the end

Thank you for joining me on this journey as we explored the exciting world of AWS Amplify and my first article on Hashnode. I hope you found inspiration and valuable insights from my experience building an app using Amplify.

If you enjoyed this article, I encourage you to explore more content on Hashnode, a vibrant community of developers and tech enthusiasts sharing their knowledge and experiences. You can find my article and many other insightful posts on the platform.

You can also make your blog. Signup here and start your blogging journey.

Additionally, if you're interested in building your projects with AWS Amplify, I highly recommend diving into the official Amplify documentation. It's a treasure trove of resources and guides to help you unleash the full potential of Amplify. You can read their docs here.

Remember, sharing your own experiences and insights is a great way to contribute to the developer community. Consider writing your articles on Hashnode and sharing your learnings with others. Together, we can continue to learn, grow, and inspire each other.

Once again, thank you for reading until the end. I hope this article has sparked your curiosity and empowered you to embark on your own Amplify projects. Happy coding!