Build an Instagram clone with strapi.js and svelte (PART 2)
Tags: svelte, webdev, tutorial, typescript
Published: 2020-11-11 17:07:24

Hey! I'm back here with part 2 of this tutorial. As promised, now, we'll be working on the frontend. 

> As always, the code is available on [Github](https://github.com/arnu515/quickstagram)

## Interacting with the Strapi API

> I was supposed to put this in the previous part, but I forgot 🤦‍♂️

We're currently using Strapi on [localhost:1337](https://localhost:1337). This is fine for now, but in production, this URL will change. If we use this URL on every request we make, we may have to change this to the production url a lot of times. Instead of having to change it so much, let's only change it once. In `main.ts`, let's pass a prop called `strapiApiUrl` to our svelte app.

```ts
// src/main.ts
import App from './App.svelte';

const app = new App({
	target: document.body,
	props: {
		strapiApiUrl: 'http://localhost:1337'
	}
});

export default app;
```

Now, in `App.svelte`, we can set this strapiApiUrl as a `context` so that we can access it from anywhere within our app. We can achieve this with `getContext` and `setContext`.

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

<script lang="ts">
    // ...
    import { setContext } from "svelte";

    // ...
    
    export let strapiApiUrl: string;
    setContext("apiUrl", strapiApiUrl);
</script>

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

### Making API requests

We can use `fetch`, but I think that `axios` is better, so let's install it:
```sh
npm install axios
```

Now, to make an API request,

```ts
import axios from "axios";
import { getContext } from "svelte";

async function makeApiRequest(): Promise<WhatTheApiReturns> {
    try {
        const { data } = await axios.get<WhatTheApiReturns>(
            getContext("apiUrl") + "/path/of/the/api"
        );
        return data;
    } catch (err) {
        console.log({ error: err });
        throw new Error(
            "Request failed with status: " +
                err.response.status +
                "\nCheck the console for further details."
        );
    }
}
```

> You can replace `get` with `post` or `delete` for those methods.

## The post component

Let's quickly scaffold up a Post component which will show our post, like on instagram:

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

Let's see the code for this Post component:

```html
<!-- src/components/Post.svelte -->

<script lang="ts">
    import { getContext } from "svelte";

    // typescript only
    import type { Post } from "../types";

    export let post: Post;
</script>

<style>
    .post {
        width: 50%;
        margin: 0 auto;
    }

    @media (max-width: 992px) {
        .post {
            width: 70%;
        }
    }

    @media (max-width: 600px) {
        .post {
            width: 90%;
        }
    }
</style>

<div class="w3-card post w3-section">
    <a href="/@{post.user.username}/{post.id}"><img
            src={post.image[0].provider === 'local' && getContext('apiUrl') + post.image[0].url}
            alt={post.image.alternativeText || 'Post image'}
            style="width: 100%" /></a>
    <div class="w3-container">
        <p class="w3-small w3-text-gray">
            <a
                href="/@{post.user.username}"
                style="text-decoration: none">@{post.user.username}</a>
        </p>
        <p>{post.content}</p>
    </div>
    <footer class="w3-bar w3-border-top w3-border-light-gray">
        <a
            href="/@{post.user.username}/{post.id}"
            class="w3-bar-item w3-button w3-text-blue"
            style="width:100%">{post.comments.length}
            {post.comments.length === 1 ? 'Comment' : 'Comments'}</a>
    </footer>
</div>
```

Now, if you want the `types.ts` file which contains the types like `Post` and `Comment`, here it is:
```ts
// WARNING: TYPESCRIPT USERS ONLY!

export interface User {
    id: string;
    username: string;
    email: string;
    provider: string;
    confirmed: boolean;
    blocked: any;
    role: number;
    created_at: string;
    updated_at: string;
}

export interface Post {
    id: number;
    user: User;
    content: string;
    image: Image;
    comments: Comment[];
    published_at: string;
    created_at: string;
    updated_at: string;
}

export interface Comment {
    id: number;
    content: number;
    user: null | User;
    post: number;
    published_at: string;
    created_at: string;
    updated_at: string;
}

interface Image {
    id: number;
    name: string;
    alternativeText: string;
    caption: string;
    width: number;
    height: number;
    formats: {
        thumbnail: ImageMetaData;
        large: ImageMetaData;
        medium: ImageMetaData;
        small: ImageMetaData;
    };
    hash: string;
    ext: string;
    mime: string;
    size: number;
    url: string;
    previewUrl: null | string;
    provider: string;
    provider_metadata: null | any;
    created_at: string;
    updated_at: string;
}

interface ImageMetaData {
    name: string;
    hash: string;
    ext: string;
    mime: string;
    width: number;
    height: number;
    size: number;
    path: null | string;
    url: number;
}
```

## Fetching posts from the API

Let's create a `/posts` route. That route will serve the `posts` component, which fetches posts from our Strapi CMS and renders them.

```html
<!-- src/routes/posts.svelte -->

<script lang="ts">
    import axios from "axios";
    import { getContext } from "svelte";
    import Post from "../components/Post.svelte";
    import type { Post as PostType } from "../types";

    async function getPosts(): Promise<PostType[]> {
        try {
            const { data } = await axios.get<PostType[]>(
                getContext("apiUrl") + "/posts"
            );
            return data;
        } catch (err) {
            console.log({ error: err });
            throw new Error(
                "Request failed with status: " +
                    err.response.status +
                    "\nCheck the console for further details."
            );
        }
    }
</script>

<h1 class="w3-xxxlarge w3-center">Posts</h1>
{#await getPosts()}
    <div class="w3-center w3-section w3-xxlarge w3-spin">
        <i class="fas fa-spinner" />
    </div>
{:then posts}
    <div class="w3-container w3-margin">
        {#each posts as post}
            <Post {post} />
        {/each}
    </div>
{:catch err}
    <div
        class="w3-panel w3-pale-red w3-padding w3-leftbar w3-border-red w3-text-red">
        {err}
    </div>
{/await}
```

We need to add this route to our router, so let's quickly do that by adding this line to `App.svelte`:

```ts
router("/posts", setupRouteParams, () => (page = Posts));
```

Now, create a post on strapi following the video below.

{% vimeo 478061498 %}

If we head over to `/posts` on our frontend, we see:

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

## Individual post page

When we visit `@username/postid`, I want to see username's post with ID `postid`, so let's do just that

```html
<!-- src/routes/onePost.svelte -->

<script lang="ts">
    import axios from "axios";
    import { getContext } from "svelte";
    import router from "page";

    import type { Post, Comment as CommentType } from "../types";
    import Comment from "../components/Comment.svelte";

    export let params: { username: string; postId: string };
    const apiUrl = getContext("apiUrl");

    async function getPost(): Promise<Post> {
        try {
            const { data } = await axios.get<Post>(
                apiUrl + "/posts/" + params.postId
            );
            if (data.user)
                if (data.user.username !== params.username)
                    router.redirect("/404");
            return data;
        } catch (err) {
            if (err.response.status === 404) router.redirect("/404");
            else {
                console.log({ error: err });
                throw new Error(
                    "Request failed with status: " +
                        err.response.status +
                        "\nCheck the console for further details."
                );
            }
        }
    }

    async function getComments(post: Post): Promise<CommentType[]> {
        try {
            let comments: CommentType[] = [];
            for (let i = 0; i < post.comments.length; i++) {
                const { data } = await axios.get<CommentType>(
                    apiUrl + "/comments/" + post.comments[i].id
                );
                comments.push(data);
            }

            return comments;
        } catch (err) {
            if (err.response) {
                console.log({ err });
                if (err.response.status === 404) router.redirect("/404");
                else {
                    console.log({ error: err });
                    throw new Error(
                        "Request failed with status: " +
                            err.response.status +
                            "\nCheck the console for further details."
                    );
                }
            } else throw new Error(err);
        }
    }
</script>

<style>
    .post {
        width: 50%;
        margin: 0 auto;
    }

    @media (max-width: 992px) {
        .post {
            width: 70%;
        }
    }

    @media (max-width: 600px) {
        .post {
            width: 90%;
        }
    }
</style>

{#await getPost()}
    <div class="w3-center w3-section w3-xxlarge w3-spin">
        <i class="fas fa-spinner" />
    </div>
{:then post}
    <div class="w3-card post">
        <a
            href={post.image[0].provider === 'local' && getContext('apiUrl') + post.image[0].url}><img
                src={post.image[0].provider === 'local' && getContext('apiUrl') + post.image[0].url}
                alt={post.image.alternativeText || 'Post image'}
                style="width: 100%" /></a>
        <div class="w3-container">
            <p class="w3-small w3-text-gray">
                <a
                    href="/@{post.user.username}"
                    style="text-decoration: none">@{post.user.username}</a>
            </p>
            <p>{post.content}</p>
        </div>
    </div>

    <div class="w3-card post w3-margin-top">
        <header class="w3-container w3-border-bottom w3-border-light-gray">
            <h3>Comments</h3>
        </header>
        <div class="w3-container">
            {#await getComments(post)}
                <div class="w3-center w3-section w3-xxlarge w3-spin">
                    <i class="fas fa-spinner" />
                </div>
            {:then comments}
                {#each comments as comment}
                    <Comment {comment} />
                {/each}
            {:catch err}
                <div
                    class="w3-panel w3-pale-red w3-padding w3-leftbar w3-border-red w3-text-red">
                    {err}
                </div>
            {/await}
        </div>
    </div>
{:catch err}
    <div
        class="w3-panel w3-pale-red w3-padding w3-leftbar w3-border-red w3-text-red">
        {err}
    </div>
{/await}
```

> We have to get each comment separately because Strapi doesn't return detailed information of **a relation inside another relation**. This can be good if you don't need this data, which in most cases, you **don't**, but if you're in situations like this, you'll have to use this method.

Now this is what our app should look like:

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

## User profile

Let's work on the user's profile page. This will be accessed through the `/@username` route. We want to display every post this user has made. So, create `src/routes/userProfile.svelte`, and put this in it:

```html
<!-- src/routes/userProfile.svelte -->

<script lang="ts">
    import axios from "axios";
    import PostComponent from "../components/Post.svelte"
import { getContext } from "svelte";
import type { Post } from "../types";

    export let params: {username: string}
    const apiUrl:string = getContext("apiUrl")

    async function getPosts(): Promise<Post[]> {
        try {
            const { data } = await axios.get<Post[]>(
                getContext("apiUrl") + "/posts"
            );
            return data.map(post => {
                if (post.user.username === params.username) return post
            });
        } catch (err) {
            console.log({ error: err });
            throw new Error(
                "Request failed with status: " +
                    err.response.status +
                    "\nCheck the console for further details."
            );
        }
    }
</script>

<h1 class="w3-xxxlarge w3-center">Posts</h1>
{#await getPosts()}
    <div class="w3-center w3-section w3-xxlarge w3-spin">
        <i class="fas fa-spinner" />
    </div>
{:then posts}
    <div class="w3-container w3-margin">
        {#each posts as post}
            <PostComponent {post} />
        {/each}
    </div>
{:catch err}
    <div
        class="w3-panel w3-pale-red w3-padding w3-leftbar w3-border-red w3-text-red">
        {err}
    </div>
{/await}
```

And here's the user's page:

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

## Conclusion

And here we are! We've seen how easy it is to integrate with Strapi's API using your frontend, but, we've only dealt with unauthenticated requests. In the next part, you'll see how to add authentication to your requests, and also create posts, comments and upload images to Strapi.

The 3rd part is out! Click [here](https://dev.to/arnu515/build-an-instagram-clone-with-strapi-and-svelte-part-3-23l2), [here](https://dev.to/arnu515/build-an-instagram-clone-with-strapi-and-svelte-part-3-23l2) or [here](https://dev.to/arnu515/build-an-instagram-clone-with-strapi-and-svelte-part-3-23l2) to view it!