Blog

Building a Netflix Clone with Piral

Published on 2021-03-19.
An extensive sample application composed with our microfrontend framework.

During the first two weeks of my international internship at smapiot, I created a Netflix clone using Piral: an open-source framework for creating modular applications.

In this article, I will go over what microfrontends are, why they are useful, and what frameworks exist to make implementing them easier. I'll also share my experiences creating a project by myself using React and Piral: two technologies I had previously never touched. I will cover what I did, and how I did it. Finally, I will present some closing thoughts, opinions, and personal notes about this endeavor.

The "How I did it" section will be written in a way where every developer, regardless of skill level, should be able to follow. Be sure to give Piral or microfrontends as a whole a try, and let me know how it went!

The home page of the application

Microfrontends

Microfrontends try to extend the idea and the benefits of microservices into the frontend space. In essence, this architecture pattern comes down to "splitting up the frontend monolith" into smaller, more easily manageable pieces.

This allows fully cross-functional teams to work on these, focussing on a specific business feature or company mission. Rather than "horizontal" teams, per layer or technology; these teams manage the "vertical" slices of the application. Each team is autonomous, and has end-to-end – from the database to the UI – responsibility for the features they develop.

Teams should be able to independently create and deploy these microfrontends. This cuts down on inter-team communication; which could then also enable distributed development.

This is especially beneficial for larger companies and projects, where the Jeff Bezos "Two Pizza Team" rule (i.e. the whole team can be fed by two pizzas) can be helpful. Spotify for example, calls these smaller feature teams "squads". Interesting read here.

Why Microfrontends?

Microfrontends make teams more agile

When comparing the characteristics and benefits of microfrontends with the 12 Agile Principles, lots of overlap emerges:

  • Autonomous teams

    Autonomous teams satisfy lots of these agile principles. In short: teams that can operate on their own are less prone to being slowed down, can make changes quickly, and feel a greater sense of ownership.

  • Incremental upgrades

    By being decoupled and decentralized, the microfrontends architecture pattern ensures that the incremental and iterative process of agile software development can succeed.

  • Independent deployment

    Microfrontends can be deployed independently. This can enable shorter release cycles, because all different parts don't have to be in sync with each other.

  • Simple and decoupled codebases

    Simplicity is essential to agility: this makes it easier for the whole team to be on board and iterate fast. Decoupling makes using different technologies possible; but even when using the same technologies throughout the app it can still be very beneficial for efficiency of development.

Piral

Piral is our open-source framework for fully flexible modular applications. It is built on React, but has lots of plugins available for other frameworks and technologies.

Building Blocks

An application built with Piral consists of multiple parts.

If you have no experience with microfrontends, this section can be confusing. Don't be alarmed: the section "The project" below will turn the abstract into the practical, which will be easier to follow.

The Pilets

These are the individual feature modules, also known as microfrontends. They each include their own dependencies and assets, and are completely independent of each other.

Pilets can define how the integration of their components will happen. Does the pilet need a dedicated page, or will the content be rendered inside an already existing pilet? Maybe we need a dedicated page, and also register a button somewhere else that will link to the page? It is all possible.

The Feed Service

Pilets are usually published to a feed service (e.g. a REST API). Piral provides its own feed service over at www.piral.cloud.

It should be noted that Piral can work without a feed service but a feed service makes deployments easy and consumption very dynamic; showcasing all the advantages of Piral.

The Piral Instance

This is the place where all feature modules will be integrated. The Piral instance will pull all registered pilets from the feed service, and put them where they need to go as defined by the pilets themselves. The app shell also is the place to put your basic layout: navbars, headers, footers, and shared components.

The result of building the app shell is a dist/release directory for hosting, and a dist/emulator directory with a tarball which can be published to an NPM registry to aid in the development and the debugging of the individual pilets.

(Component) Extensions, Pages and Menu Items

The Pilet API supports registering extensions in your pilets and Piral instance. Let's say for example we have a webshop with 2 pilets: a discover pilet that lists products and a checkout pilet that enables users to purchase these items (this is by the way a classic example for microfrontends, read more here). The discover pilet should include a button to purchase items, but since that is not the responsibility of this team, the checkout team will create this button and register it as an extension that all pilets can use. The discover pilet will then just register an extension slot where the app shell will integrate the right extension into.

Piral also has a built-in way to register pages and menu items. These can also be seen as extensions, but where the work is already done for you.

The Project

Application Overview

You can find the application online on netflix.deruwe.me.

This application is a Netflix clone with some basic functionalities. There is a Browse page where the user can discover showcases of trending series and movies, top-rated ones, etc.

Of course, to find a specific movie or series, the user can also use the provided Search bar.

Every media tile also has a Favorites toggle in the top right corner. Clicking it adds the series or movies to the user's favorites list, to be found on the favorites page.

The user can switch accounts via the Profile option in the top right. All favorites are linked to the specific account.

It is worth noting that this demo project does not come with a custom backend: all data is coming from a 3rd party API, the accounts are dummy accounts, and the favorites are stored in local storage.

Impressions

The Browse and the Favorites pages
The Profile page

Structure of the Application

The App Shell

The app shell contains only the logo, navigation, and footer. All the other components are provided by the pilets in the form of extensions, pages, and menu items.

The Pilets

PiletRegistered components
Browse
  • Menu item
  • Browse (page)
  • Favorites
  • Menu item
  • FavoritesToggle (component extension)
  • Watch
  • MovieTile (component extension)
  • Player (page)
  • Search
  • Search (component extension)
  • Profile
  • UserProfile (component extension)
  • AccountSwitcher (page)
  • Throughout the creation of the project using Piral, obviously, the Piral documentation was my main source of inspiration. There, they also have video tutorials on lots of topics regarding Piral.

    The Piral documentation also talks about the 3 phases of the development workflow. This is also the way I tried to develop my application. Of course, to be able to experiment, I sometimes stepped a bit of out bounds.

    0. The Planning Phase

    But before following any of the laid-out steps provided by Piral, I looked out for a sample project that I could build upon. I'm not a designer, so looking for a React project with good styling was the easiest option. I found this project, which was written using an older React syntax, and was all in one big App module. I converted everything into separate React functional components. This was a great way to learn how React works.

    You can see the results in this repo. The commit history here shows what I did.

    1. The Setup Phase

    What needs to be done in this phase?

    1. Develop the Piral instance
    2. Set up a feed service and connect the Piral instance to it
    3. Distribute an emulator package

    Following the documentation showed me how to get this up and running. Install the Piral CLI globally by running:

    npm install piral-cli -g

    You could also use npx to avoid unsafe global installations, see below.

    The CLI exposes commands starting with piral and pilet. In this phase, of course, we will need the piral commands.

    To create a Piral instance (app shell) called netflix-piral, let's run

    piral new --target netflix-piral

    We can run the newly created Piral instance with the following command:

    piral debug
    
    # or, if you want to open the browser automatically: piral debug --open

    Instead of a command like piral debug which only works inside a directory with a package.json or if you have the Piral CLI installed globally, you can also use npx:

    • if the given name is not available in (modified) path (e.g., global path or node_modules/.bin), npx will try to get the command from NPM and run it (non-globally i.e. from user privileges)
    • if the given name is available it will just run it from there (also from user privileges)

    Let's have a look at one of the most important files, index.tsx:

    The renderInstance function outlines the responsibilities of the app shell: it takes care of the layout, the error layout, and requests the pilets from a feed service. As we can see on line 6 - by default - it's just pulling from an empty feed.

    In fact, the only thing that will change in this file, is the feed URL. To be able to do that: let's first set up a feed.

    While you could (and in some circumstances, should) set up your own feed service, most of the time the service provided by the Piral team itself will suffice. For development purposes, you get multiple feeds for free! This service can be found on www.piral.cloud.

    To create a feed on www.piral.cloud you need to Sign in to the service and you'll see the following screen:

    Of course, we're going to click + New Feed. Next, we'll give the feed a unique name (which cannot be changed), and optionally, a description so it's clear for what this feed will be used. You can also configure the allowed hosts.

    You'll see the result on the overview:

    To be able to publish pilets later, we'll need an api key. You can manage them by clicking

    To get the feed url for the app shell, we can click the feed title. The url will be displayed:

    We'll copy the feed url and place it where we wanted it before: in the index.tsx of the Piral instance (line 6).

    We have an app shell now which pulls from our own (still empty) feed! We'll add pilets to this feed later. But first, maybe we should customize the layout of this app shell. As written before, the main responsibilities we want for this app shell are the logo, the navigation, and the footer.

    After scaffolding, the layout.tsx file contains a lot of components and also combines them in a layout object to be used by the index.tsx. While this is fine, I like to split up all my components using a single file per component.

    We'll put the layout in ./components/App.tsx, the navigation template in ./components/Navigation.tsx and for the menuitems, they are just rendered using <li>...</li>.

    Remember what I mentioned before:

    The app shell contains only the logo, navigation, and footer. All the other components are provided by the pilets in the form of extensions, pages, and menu items.

    This is absolutely the case, but we do however need to define where the pilets need to render these extensions! Here is a quick wireframe diagram for the app shell.

    The pages registered by the pilets will just be given to the App component as children.

    As for the extensions: The key to being able to integrate these is an ExtensionSlot with a specific name. The pilets are then able to register extensions, providing a name, and the app shell will put them in the right slot.

    The menu items are standardized in Piral. The component registered in index.tsx as the MenuContainer (= in our case, the Navigation component) will get menu items as children when pilets register them.

    To deploy the application for the world to see, we need to publish it somewhere. To me, the best place to do this is Netlify. One could of course choose Azure Static Web Apps, Github pages, or another hosting platform, but Netlify is easy to use and has a lot of great features that are completely free.

    To get started, create an account on Netlify. I like to use my Github account because this way the accounts are already linked.

    Next, create a "New site from git" in the sites tab of the interface.

    Find the Github repository of your app shell. If you don't have one already... you should create one ;)

    Now configure the build settings as follows:

    • set a branch (I use master, you could also create a custom release branch)
    • set the build command to npm run build or piral build or piral build --type release
    • set the publish directory to /dist/release/ (don't skip this step!)

    Then you are ready to deploy your site with the click of a button! Now every time you push your code to the selected branch, the site will be updated! CI/CD for the win!

    When you deploy the app shell for the first time, you will not notice it, but the routing is not perfect. To save yourselves some headaches later on, you best follow the next steps already, so you won't have to touch your app shell again.

    If you go to yourwebsite.netlify.app/test, Netlify will try to find a test.html page to serve you, will not find it, and show an error message. We want React Router to deal with routes. We have to redirect all routes to the index.html... To do this, we create a folder with path /src/static/ and put a _redirects file into it:

    To make sure this file is copied to the release directory on build, we need to configure webpack to do so.

    Install the CopyWebpackPlugin

    npm install copy-webpack-plugin --save-dev

    In the root folder of your project, create webpack.config.js

    This will copy everything from the src/static/ directory to the build directory. This means you can later on also add images and other files to this static directory if you so desire.

    Now, we have our app shell up and running. When pushing Pilets to our feed service, the app shell can access these immediately and the site will be updated. But what if we want to develop new pilets? Surely we won't be publishing them a hundred times to see how they look, right?

    Luckily, Piral has a good solution to this: an app shell emulator. The pilets can use the emulator to see how they will look when integrated into the app shell, to be able to quickly debug the pilets.

    To create an app shell emulator, run

    piral build --type emulator

    The emulator is a .tar.gz or .tgz file (a so-called "tarball") and can be found in the /dist/emulator/ directory.

    Great. Now we have a file. If we are creating pilets alone, on one pc, this is no big deal. But ideally, we want the emulator to be accessible from every pilet, and also be able to update the emulator when a new version of the app shell is necessary. That's why it makes sense to publish the emulator.

    To be able to access the emulator from everywhere, we are going to use the node package manager or npm. First, go to npmjs.org and create an account if you don't already have one.

    Next, in your terminal, run

    npm login

    and log in using your username and password. Next, you can run

    npm publish dist/emulator/<emulator_file>

    The <emulator_file> will in our case be netflix-piral-1.0.0.tgz or something similar. If you get an error (which could mean the name you chose is already taken), refer to this article or the npm docs.

    If you look at your registered packages on npmjs.org, you should be able to see the published emulator package! This will be very useful in the next phase: the feature phase, where the development of the pilets will be addressed.

    2. The Feature Phase

    What needs to be done in this phase?

    1. Build and publish pilets to enable functionalities in the app.
    2. Manage separation of concerns
      • extract app shell functionality into pilets
      • split larger pilets or merge smaller ones

    Creating a pilet is really straightforward. The Piral CLI provides an easy way to scaffold a pilet based on a Piral instance. For us the workflow will look like this:

    mkdir browse
    cd browse
    pilet new netflix-piral

    This will create a folder browse, and put a new pilet called "browse" – which is based on the Piral instance netflix-piral – inside of it.

    Let's create some functionalities! The home page of this app will be the "browse" page. Since discovering new series and letting the user browse through series and movies is a pretty big part of the app, this will be the responsibility of one pilet (and, by consequence, a separate dev team).

    The file structure looks like this:

    A pilet is very lightweight. The only file to look at is the index.tsx, where some interesting examples of the Piral API are shown:

    The setup function is the heart of the pilet. This is where the app shell will look for instructions for integrations.

    We won't need the notifications or the tiles. You can learn more on these from the Piral documentation.

    The most interesting method for us is registerMenu, we'll need this for the "Browse" menu item:

    app.registerMenu(() => <Link to="/browse">Browse</Link>);

    And to register a page where this menu item can link to, we will need to add

    app.registerPage('/browse', Browse);

    Where this Browse is just a regular React component (for now). The structure looks a bit like this:

    Browse
    ├── Hero
    ├── Showcase
    │ ├── MovieTile
    │ └── ... #more movietiles
    └── ... #more showcases with movietiles

    To be able to test how the pilet will look after integration into the app shell, of course, we could just publish it and look at the live website. However, I won't have to explain why "testing in production" is not the best idea ever.

    So, Piral offers a way to debug the pilet, this is where the emulator comes into play. To debug the pilet, you can run

    pilet debug

    After the build process is complete, the CLI will let you know on what local address you can look at the result (usually http://localhost:1234).

    It's interesting to note that this command is almost identical to the one for the app shell, but there we used the piral keyword, and now we use pilet.

    We already published the Piral instance (app shell), and the fun thing about working with Piral is that this app shell will pull every pilet from a feed and integrate them client-side.

    This means, to publish a pilet, we won't have to touch deployment stuff. We just need to publish the pilet to the feed we created earlier.

    We can do this by:

    pilet publish --fresh --url <feed_url> ---api-key <feed_api_key>

    Tip: I saved this snippet as a script called publish.sh, added it to .gitignore (so my API key won't be on Github), and then ran the script whenever I wanted to publish.

    The --fresh flag makes sure that before publishing, a fresh build is made to include any changes made after the last build.

    The feed url and API key, as mentioned before, you can find in the Piral Feed Service dashboard. The direct URL is:

    https://www.piral.cloud/feeds/<feed_name>/api-keys

    Next, let's tackle a more interesting case. The profile pilet. This pilet will again register a page, /profile, but will also do something else: it will register a component extension.

    When looking back at the app shell, this component extension has to be put in the extension slot header-items. so that's what we will do.

    As is the deal with component extensions: the app shell will integrate the registered extension into the right ExtensionSlot (the one with a matching name).

    Here we start to run into an interesting thing. We want to introduce the favorites as a page where we can find all the favorite series or movies. This means multiple things:

    1. Just like in the Browse component, we will need a way of displaying media (MovieTile)
    2. We will need to provide a FavoritesToggle button in every MovieTile, to be able to toggle this item as a favorite

    We could just copy over the MovieTile code from the browse component and reuse it here. This would be a very viable strategy, and it's also the one I used when you look back in the commit history.

    That being said, later on in the project, I looked back at this part in the Piral docs:

    "Determine when to split pilets and potentially split larger pilets or merge smaller ones."

    That's when it started making sense to extract the MovieTiles into a separate watch pilet, where they are registered as component extensions. I'll talk about the watch pilet in the next section.

    We'll offer the favorites button as a component extension, so all pilets or the app shell can integrate this button wherever they want them.

    For that, we need this in the setup function of the favorites pilet:

     app.registerExtension('ListToggle', props => <FavoriteToggle ...props.params></FavoriteToggle>);

    This is where passing parameters into component extensions becomes interesting.

    (If you want to see the full code, check the github repo, I'm omitting stuff for brevity)

    For the FavoriteToggle toggle function, it is important that the button gets some properties. Using a favourites toggle may look like this:

    <FavoritesToggle movieId="15165" media_type="tv" />

    or something similar. All this introduction leads us to the main question: how to pass parameters to component extensions when using them across pilets?

    Well, it's pretty easy: the Extensionslot component has a property params. Whenever we want to use an extension, we give the slot the params, and Piral will pass these params to the extension that will end up in that slot. This means, a registered extension will have props.params, which will come from wherever we defined the extension slot.

    If we want to use this component extension from other pilets, the extension slot will have to look something like:

    <ExtensionSlot name="ListToggle" params={/*an object with the params here*/}/>

    We will see an example and best practices about this in the next section:

    This pilet would have 2 things registered:

    • the MovieTile we talked about earlier.
      • this should have a spot where our FavoritesToggle component extension can fit into!
    • the Player (which is just a simple page and we won't discuss further)

    The MovieTile was an interesting lesson in what I like to call extensionception: we'll register a component extension, but within that extension, we'll use an ExtensionSlot where another component extension will fit into:

    The eventual result on for example the favorites page will look like this:

    Ok, let's look at the MovieTile component.

    This component accepts a whole bunch of properties to be able to display the movie tiles with all the information. It's a purely presentational component.

    On line 11 you can see that the MovieTileProps also contain a definition for a React component reference: this will be the FavoritesToggle we defined before.

    But why don't we just put <Extensionslot name="ListToggle"/> there? Well, it's because of what I learned while reading the Pilet best practices on using extensions

    Using components provided from other pilets is done via "extensions". The problem is that the extensions require the Extension component of the Pilet API to be integrated.

    Indeed. We would need to do this at the top of our component extension file

    import { ExtensionSlot } from 'piral';

    This is a bad practice: we couple our components to the Pilet API, and now they are no longer reusable, testable, and generic.

    The fix comes down to this: The only file in a pilet that should depend on the Piral framework is the index.tsx file with the setup function. From there, we can pass down the needed dependencies.

    On line 10, we use app.Extension, which has the same purpose as an ExtensionSlot. We use the result of that to have a component to pass into another component. This way, the MovieTile has props.Toggle defined, and can use it just like any other React component.

    While developing the browse pilet, the section where I talked about debugging was called "debugging the pilet in isolation". Now, we're going to do something more powerful.

    Let's recall what happens when we run pilet debug. We have an app shell emulator in which the pilet will be integrated. That's it – 2 parts:

    • the app shell (emulated)
    • the pilet that's being debugged

    But what if we want to see the already published pilets too, to see how the pilet that we are debugging will fit into them? (mainly, in this case, we want to see how the extensions integrate)

    At the time of writing, the Piral CLI is still in version v0.12.4, but I got the recommendation to switch to the v1.0.0 preview version (the @next version). This version of the CLI provides in my opinion a major game-changing feature: the ability to debug pilets, while also being able to include remote pilets from the feed!

    It's also very easy to do:

    pilet debug --feed <feed_url>

    And voila! We can see how the new pilet will fit into the app shell and the already defined pilets in the feed! Amazing!

    Honestly, since learning about this feature, I never ever used debugging in isolation again. It's so much easier to see how the pilet will fit into the application when also including other pilets into view.

    To make my life easier, this is what my scripts looked like in every pilet's package.json:

     "scripts": {"debug": "pilet debug --feed <feed_url>"},

    This way, I could just run the command npm run debug!

    This pilet just registers one component extension. We'll also set it to render into the header-items slot. This way: we will get the search and the profile extension both in there.

    3. The Maintenance Phase

    This is mainly bug fixing and doing optimizations.

    This has nothing to do with Piral, but I wanted to store some data via local storage and I ran into a pretty cool way to do this by using this custom React hook.

    In the pilet setup function, we can set pages to lazily load. This is related to bundle splitting: more info here.

    e.g.,

    const ProfilePage = React.lazy(() => import('./components/ProfilePage')); app.registerPage('/profile', ProfilePage);

    If time was spent thinking about the responsibilities of the app shell before developing the first pilets, you can save yourself a lot of headaches. Though it is possible that the app shell needs to be updated. Of course, the pilets that depend on the app shell emulator for debugging would need to get an update as well!

    Luckily, this is fairly simple

    • the app shell is updated, built, and the update is pushed to npm
    • in the pilet, run pilet upgrade to pull in the latest version of the emulator

    Final Thoughts

    While I had zero experience using React and Piral before doing this project, I think the project turned out really well.

    When working with microfrontends, the biggest hurdle is getting to the big picture. To me, it was really complicated to imagine how all the microfrontends would come together.

    I saw this video recently and it really stuck with me. When trying to understand hard concepts: treat them like a black box first, and learn how to use them, before learning about how they work.

    The experience you get by using a concept will give you a major advantage while learning how they work because you will already understand the desired outcome.

    The key to understanding microfrontends – in my opinion – is to build some! Once you see visually how they all come together, it's easier to imagine how this integration is happening. This is why a microfrontend framework is valuable. Not only does it provide the best developer experience, but also: lots of stuff is already done for you, and you can get started easily.

    This analogy, by the way, also makes sense when explaining how I learned to work with React in just one week. Rather than starting from scratch, I just tweaked an already existing project, and that already got me to understand lots of the concepts. (Of course, my experience with Angular helped a little as well)

    Dante De Ruwe
    Dante De RuweTrainee