Exploring strapi.js - Build an instagram clone with strapi and svelte
Tags: tutorial, webdev, svelte, typescript
Published: 2020-11-11 07:14:48

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](https://strapi.io)!

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](https://instagram.com) 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](https://firebase.google.com), 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](https://nodejs.org) installed, which I hope you all do already. To create a strapi app, we execute this command:
```sh
# cms is the name of the folder that strapi will be in
npx create-strapi-app cms --quickstart
cd cms
npm run develop
```

> 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](https://svelte.dev) 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.
```sh
npx degit sveltejs/template frontend
cd frontend
# Open in VSCode
code .
# Make app into typescript
node scripts/setupTypescript.js
npm i
```

While we're setting up the frontend, let's also add a router like Page.js to make an SPA app.
```sh
npm i page
# for typescript only
npm i -D @types/page
```

## Basic boilerplate for the frontend

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

```html
<!-- 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>
```

```html
<!-- 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} />
```

```html
<!-- 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>
```

```html
<!-- 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>
```

```html
<!-- 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>
```

```html
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>
```

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

```html
<!-- 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)} />
```

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

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

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

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

    // ...
</script>

<!-- ... -->
```

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`:

```json
"scripts": {
    "build": "rollup -c",
    "dev": "rollup -c -w",
    "start": "sirv public -s --host",
    "validate": "svelte-check"
}
```

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](https://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:

{% vimeo 477601858 %}

> 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.

{% vimeo 477604197 %}

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

{% vimeo 477607666 %}

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.

{% vimeo 477904401 %}

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

## Accessing the API

We need a program to make API requests like [Insomnia](https://insomnia.rest) or [Postman](https://postman.com) (*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>
```

To get the collection name for any endpoint:

{% vimeo 477906170 %}

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

![Get posts (Insomnia)](https://dev-to-uploads.s3.amazonaws.com/i/zvd65tgruvhsyrmouzkh.png)

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:

{% vimeo 477907184 %}

Now, if we rerun the API request:

![Get posts (working version) (Insomnia)](https://dev-to-uploads.s3.amazonaws.com/i/vu696kunc4s10d1k4ig2.png)

We can see that it works!

Same thing in cURL:

```sh
$ curl -X GET http://localhost:1337/posts
[]
```

We can also do the same thing with comments:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/mhn4aq10n68sxf67r3l6.png)

### 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](https://dev-to-uploads.s3.amazonaws.com/i/h3xq3u4iwjdrntz8bk6b.png)

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

Same thing with cURL

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

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](https://dev-to-uploads.s3.amazonaws.com/i/so9ewookt1c80ai8u1a4.png)

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:

```sh
$ 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"}
```

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](https://dev-to-uploads.s3.amazonaws.com/i/vz7gtmsgf4r12n3ug74b.png)

> If your token **expires**, you can log in again by sending a > `POST` request to `/auth/local`. Eg:
> ```sh
> 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](https://dev.to/arnu515/build-an-instagram-clone-with-strapi-js-and-svelte-part-2-44ld)