Exploring strapi.js - Build an instagram clone with strapi and svelte

arnu515

arnu515

Posted on November 11, 2020

Exploring strapi.js - Build an instagram clone with strapi and svelte

Hello wonderful people of DEV! Thanks for 50 followers by the way, you're all awesome! I'm back with another tutorial for you guys, and today, I've got Strapi!

Strapi is a CMS (Content Management System), like Wordpress, except, Strapi is headless, meaning it has a bunch of APIs that allow you to use its features without needing to be restricted to its frontend. Basically, Strapi is a BaaS that we host ourselves. We don't have to write any backend code to use Strapi. All we have to do, is install it on our machine.

So, today, I'll show you how you can make an Instagram clone using Strapi as the backend and any framework you want (I've gone with Svelte) as the frontend. You can checkout the live app [here]. The source code for the project is available [here].

Creating a Strapi project

If you remember, in my last tutorial, when I showed you serverless with Firebase, we had to sign up to Firebase, create a project, create an app, and finally add the Firebase config to our project.

This time, it is much simpler. You will need NodeJS and NPM installed, which I hope you all do already. To create a strapi app, we execute this command:

# cms is the name of the folder that strapi will be in
npx create-strapi-app cms --quickstart
cd cms
npm run develop
Enter fullscreen mode Exit fullscreen mode

Reminds you of create-react-app, doesn't it?

This will create a Strapi app in the cms folder, go to that folder and start the app. You can leave this terminal window running in the background.

The --quickstart option makes it easier for us to get the app running by using SQLite as our database.

Creating our frontend application

Let's now focus on the frontend. I'm gonna use Svelte for the frontend, since I've fallen in love with it. Svelte is easy to understand, so you can translate it to the framework you're using. I'm also going to be using typescript as the language, instead of javascript, so be careful, and remove any type assertions or interfaces and stuff like that if you'll be using javascript.

To create the svelte app, we can use degit.

npx degit sveltejs/template frontend
cd frontend
# Open in VSCode
code .
# Make app into typescript
node scripts/setupTypescript.js
npm i
Enter fullscreen mode Exit fullscreen mode

While we're setting up the frontend, let's also add a router like Page.js to make an SPA app.

npm i page
# for typescript only
npm i -D @types/page
Enter fullscreen mode Exit fullscreen mode

Basic boilerplate for the frontend

Now, let's add the basic boilerplate. Stuff like adding styles, the frontpage UI, and adding the router.

<!-- public/index.html -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset='utf-8'>
    <meta name='viewport' content='width=device-width,initial-scale=1'>

    <title>Quickstagram - Instagram, but quick!</title>

    <link rel='icon' type='image/png' href='/favicon.png'>
    <link rel='stylesheet' href='/global.css'>
    <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
    <link href="https://use.fontawesome.com/releases/v5.0.1/css/all.css" rel="stylesheet">
    <link rel='stylesheet' href='/build/bundle.css'>

    <script defer src='/build/bundle.js'></script>
</head>
<body>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode
<!-- src/App.svelte -->

<script lang="ts">
    import { setContext } from "svelte";
    import router from "page";
    import { parse } from "qs";
    import Index from "./routes/index.svelte";
    import Navbar from "./components/Navbar.svelte";

    export let strapiApiUrl: string;

    let page;
    let params;
    let queryString;

    function setupRouteParams(ctx: PageJS.Context, next) {
        params = ctx.params;
        queryString = parse(ctx.querystring);
        next();
    }

    router("/", setupRouteParams, () => (page = Index));

    router.start();

    setContext("apiUrl", strapiApiUrl);
</script>

<Navbar />

<!-- This component just renders the component `this`. It is used to render components dynamically, like how we're doing -->
<svelte:component this={page} {params} {queryString} />
Enter fullscreen mode Exit fullscreen mode
<!-- src/components/Index.svelte -->

<script lang="ts">
    import Auth from "../components/Auth.svelte";

    export const queryString = {};
    export const params = {};
</script>

<div class="w3-container">
    <h1 class="w3-center w3-xxxlarge">Quickstagram</h1>
    <p class="w3-center w3-large w3-text-gray">Instagram, but quicker!</p>

    <div class="w3-center">
        <a
            href="/auth?action=register"
            class="w3-button w3-blue w3-border w3-border-blue w3-hover-blue">Register</a>
        <a
            href="/auth?action=login"
            class="w3-button w3-white w3-border w3-border-black w3-hover-white">Login</a>
    </div>

    <Auth />
</div>
Enter fullscreen mode Exit fullscreen mode
<!-- src/components/Navbar.svelte -->

<script lang="ts">
    import { slide } from "svelte/transition";
    let active = false;
</script>

<style>
    .toggler {
        display: none;
    }

    @media (max-width: 600px) {
        .logo {
            display: block;
            width: 100%;
        }
        .logo .toggler {
            float: right;
            display: initial;
        }
        .nav {
            display: flex;
            width: 100%;
            flex-direction: column;
        }

        .nav a {
            text-align: left;
        }
    }
</style>

<div class="w3-bar w3-blue">
    <div class="logo">
        <a
            href="/"
            class="w3-bar-item w3-text-white w3-button w3-hover-blue">Quickstagram</a>
        <button
            class="toggler w3-button w3-blue w3-hover-blue"
            on:click={() => (active = !active)}>
            <i class="fas fa-{active ? 'times' : 'bars'}" /></button>
    </div>
    <div class="w3-right w3-hide-small">
        <a href="/upload" class="w3-bar-item w3-button w3-hover-blue">Upload</a>
        <a
            href="/auth?action=login"
            class="w3-bar-item w3-button w3-hover-blue">Login</a>
        <a
            href="/auth?action=register"
            class="w3-bar-item w3-button w3-purple w3-hover-purple">Register</a>
    </div>
    {#if active}
        <div class="w3-right nav w3-hide-large w3-hide-medium" transition:slide>
            <a
                href="/upload"
                class="w3-bar-item w3-button w3-hover-blue">Upload</a>
            <a
                href="/auth?action=login"
                class="w3-bar-item w3-button w3-hover-blue">Login</a>
            <a
                href="/auth?action=register"
                class="w3-bar-item w3-button w3-purple w3-hover-purple">Register</a>
        </div>
    {/if}
</div>
Enter fullscreen mode Exit fullscreen mode
<!-- src/components/ErrorAlert.svelte -->

<script lang="ts">
    export let message: string;
</script>

<div
    class="w3-panel w3-pale-red w3-leftbar w3-border-red w3-text-red w3-padding">
    {message}
</div>
Enter fullscreen mode Exit fullscreen mode
src/components/Auth.svelte

<script lang="ts">
    import Error from "./ErrorAlert.svelte";
    import { fade } from "svelte/transition";

    type AuthMode = "login" | "register";

    export let authMode: AuthMode = "register";

    let loginError: string | null = null;
    let registerError: string | null = null;

    let email = "";
    let password = "";
    let cpassword = "";
    let username = "";

    function login() {
        email = email.trim();
        password = password.trim();

        if (!email || !password) {
            loginError = "Fill out all fields!";
            return;
        }
        loginError = null;
    }

    function register() {
        email = email.trim();
        password = password.trim();
        cpassword = cpassword.trim();
        username = username.trim();

        if (!email || !password || !cpassword || !username) {
            registerError = "Fill out all fields!";
            return;
        }
        registerError = null;
    }
</script>

<style>
    .auth-box {
        width: 40%;
        margin: 1rem auto;
    }

    @media (max-width: 600px) {
        .auth-box {
            width: 80%;
        }
    }
</style>

<div class="w3-container">
    <div class="w3-card-4 w3-border w3-border-black auth-box">
        <div class="w3-bar w3-border-bottom w3-border-gray">
            <button
                style="width: 50%"
                on:click={() => (authMode = 'login')}
                class="w3-bar-item w3-button w3-{authMode === 'login' ? 'blue' : 'white'} w3-hover-{authMode === 'login' ? 'blue' : 'light-gray'}">Login</button>
            <button
                style="width: 50%"
                on:click={() => (authMode = 'register')}
                class="w3-bar-item w3-button w3-{authMode === 'register' ? 'blue' : 'white'} w3-hover-{authMode === 'register' ? 'blue' : 'light-gray'}">Register</button>
        </div>
        <div class="w3-container">
            <h3>{authMode === 'login' ? 'Login' : 'Register'}</h3>

            {#if authMode === 'login'}
                <form on:submit|preventDefault={login} in:fade>
                    {#if loginError}
                        <Error message={loginError} />
                    {/if}
                    <div class="w3-section">
                        <label for="email">Email</label>
                        <input
                            type="email"
                            bind:value={email}
                            placeholder="Enter your email"
                            id="email"
                            class="w3-input w3-border w3-border-black" />
                    </div>
                    <div class="w3-section">
                        <label for="password">Password</label>
                        <input
                            type="password"
                            bind:value={password}
                            placeholder="Enter your password"
                            id="password"
                            class="w3-input w3-border w3-border-black" />
                    </div>
                    <div class="w3-section">
                        <button
                            class="w3-button w3-blue w3-hover-blue w3-border w3-border-blue">Login</button>
                        <button
                            class="w3-button w3-white w3-hover-light-gray w3-border w3-border-black"
                            on:click={() => (authMode = 'register')}>Register</button>
                    </div>
                </form>
            {:else}
                <form on:submit|preventDefault={register} in:fade>
                    {#if registerError}
                        <Error message={registerError} />
                    {/if}
                    <div class="w3-section">
                        <label for="username">Username</label>
                        <input
                            type="text"
                            bind:value={username}
                            placeholder="Enter a username"
                            id="username"
                            class="w3-input w3-border w3-border-black" />
                    </div>
                    <div class="w3-section">
                        <label for="email">Email</label>
                        <input
                            type="email"
                            bind:value={email}
                            placeholder="Enter your email"
                            id="email"
                            class="w3-input w3-border w3-border-black" />
                    </div>
                    <div class="w3-section">
                        <label for="password">Password</label>
                        <input
                            type="password"
                            bind:value={password}
                            placeholder="Enter a password"
                            id="password"
                            class="w3-input w3-border w3-border-black" />
                    </div>
                    <div class="w3-section">
                        <label for="cpassword">Confirm Password</label>
                        <input
                            type="password"
                            bind:value={cpassword}
                            placeholder="Re-enter that password"
                            id="cpassword"
                            class="w3-input w3-border w3-border-black" />
                    </div>
                    <div class="w3-section">
                        <button
                            class="w3-button w3-blue w3-hover-blue w3-border w3-border-blue">Register</button>
                        <button
                            class="w3-button w3-white w3-hover-light-gray w3-border w3-border-black"
                            on:click={() => (authMode = 'login')}>Login</button>
                    </div>
                </form>
            {/if}
        </div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Now, let's also add a /auth route that just renders our auth component.

<!-- src/routes/auth.svelte -->

<script lang="ts">
    import Auth from "../components/Auth.svelte";
    import router from "page";

    export const params = {};
    export let queryString: { action: "login" | "register"; next: string };
</script>

<Auth authMode={queryString.action} on:auth={() => router.redirect(queryString.next)} />
Enter fullscreen mode Exit fullscreen mode

We need to register this auth.svelte component in our router, so let's do just that:

<!-- src/App.svelte -->

<script lang="ts">
    // ...
    import Auth from "./routes/auth.svelte";
    // ...

    router("/", setupRouteParams, () => (page = Index));
    router("/auth", setupRouteParams, () => (page = Auth));

    // ...
</script>

<!-- ... -->
Enter fullscreen mode Exit fullscreen mode

You may notice that the routes don't work. This is because our app isn't configured to be SPA compatible yet. Let's do so. Edit your package.json:

"scripts": {
    "build": "rollup -c",
    "dev": "rollup -c -w",
    "start": "sirv public -s --host",
    "validate": "svelte-check"
}
Enter fullscreen mode Exit fullscreen mode

Rerun your app with npm run dev.

Configuring strapi

We need to create models for our database since we're using an SQL database. Fortunately, Strapi makes it easy. Head over to the Strapi admin panel at localhost:1337/admin.

Make sure your server is still running! If not, run it with:

npm run develop

Creating a post modal

Let's define our Post, i.e. what should a post contain. Since this is an Instagram clone, we can ask "What does an Instagram post contain?" It contains:

  • The image of the post
  • The post's author
  • The post's text
  • The post's likes
  • The post's comments
  • When the post was created

Let's create a post collection in Strapi. Follow the steps in the video below:

In the video above, I created a created column. This is not required, because Strapi does it automatically at that time.

Now, we need comments. Let's create a comment collection.

We can also set certain properties to fields (called columns in SQL).

Now, for the selling point of SQL, relations. Let's add special columns called relations that allow us to reference another table using that field.

And we're done! Let's now access the Strapi API!

Accessing the API

We need a program to make API requests like Insomnia or Postman (I'm gonna go with the former). If you're using a *nix system like MacOS/Linux, you can also use the cURL command.

Unauthenticated requests

Anybody should be able to access parts of our API without logging in, for example, they can access the Posts, Images and comments, but should not be able to delete or upload them. Let's try accessing the posts. To access any collection from the API, the endpoint will be:

GET http://localhost:1337/<collection_name>
Enter fullscreen mode Exit fullscreen mode

To get the collection name for any endpoint:

Great! Now that we have the collection name, let's call the endpoint http://localhost:1337/posts

Get posts (Insomnia)

What! We get a 403 FORBIDDEN error. Why so? This is because we haven't set up any rules yet. Rules determine who gets to see what. Let's change the rules for post and comment to this:

Now, if we rerun the API request:

Get posts (working version) (Insomnia)

We can see that it works!

Same thing in cURL:

$ curl -X GET http://localhost:1337/posts
[]
Enter fullscreen mode Exit fullscreen mode

We can also do the same thing with comments:

Alt Text

Authenticated requests

Now, let's focus on users who are authenticated. An authenticated user should be able to do the same things as an unauthenticated user, but, they can also create posts and comments. If we try creating a comment by sending a POST request to http://localhost:1337/comments, we get a 403 FORBIDDEN error.

Alt Text

Remember to set Content-Type header to application/json!

Same thing with cURL

$ curl -X POST -H "Content-Type: application/json" -d '{"content": "Hello world!"}' http://localhost:1337/comments
{"statusCode":403,"error":"Forbidden","message":"Forbidden"}
Enter fullscreen mode Exit fullscreen mode

Let's fix that by authenticating. When we authenticate, we get a JSON Web Token back that we can then attach to other requests using the Authorization header. To authenticate, we need to send a POST request to http://localhost:1337/auth/local. This POST request should contain an identifier field, which can either be the email or username of the user, and a password field, which is the user's password.

We don't have any users. Let's register one:

Alt Text

You can see that we got back a token. This token is the JSON Web Token I was talking about earlier. Let's use this in our request from earlier to create a new post:

$ curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer paste_your_token_here" -d '{"content": "Hello, world!"}' http://localhost:1337/comments
{"id":1,"content":"Hello, world!","user":null,"post":null,"published_at":"2020-11-11T07:07:26.552Z","created_at":"2020-11-11T07:07:26.564Z","updated_at":"2020-11-11T07:07:26.564Z"}
Enter fullscreen mode Exit fullscreen mode

The comment has been successfully added! Congratulations! Let's look at the Strapi Admin Panel. We can see that our changes have been reflected!

Alt Text

If your token expires, you can log in again by sending a > POST request to /auth/local. Eg:

curl -X POST -H "Content-Type: application/json" -d '{"identifier": "your email", "password": "your password"}' http://localhost:1337/auth/local

Conclusion

That was a look at strapi.js. This is the first part of this series. In the next part, we'll get to the frontend and other juicy stuff!

View the second part

💖 💪 🙅 🚩
arnu515
arnu515

Posted on November 11, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related