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
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. 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.
// 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
.
<!-- 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:
npm install axios
Now, to make an API request,
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
withpost
ordelete
for those methods.
The post component
Let's quickly scaffold up a Post component which will show our post, like on instagram:
Let's see the code for this Post component:
<!-- 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:
// 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.
<!-- 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
:
router("/posts", setupRouteParams, () => (page = Posts));
Now, create a post on strapi following the video below.
If we head over to /posts
on our frontend, we see:
Individual post page
When we visit @username/postid
, I want to see username's post with ID postid
, so let's do just that
<!-- 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:
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:
<!-- 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:
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.
Top comments (11)
For those people getting an error stating
cannot read property provider of undefined
, you have to putpost.image
instead ofpost.image[0]
. I accidently set the type of image on post to multiple in strapi. If you still have questions, check on the Github repository: github.com/arnu515/quickstagramHow to handle following / followers in strapi?
You could create a followers table in the database, which would contain the user it belongs to, and users they follow as relationships. This will allow you to get the Followers and Following numbers you see on instagram.
For the frontend, you could setup a custom filter which would show, for example, only the posts of the people the user followed.
Hey there, I know it is a super late reply!
but can you elaborate here, I'm primarily a frontend dev!
No worries! Strapi makes it really easy for you.
You need to create a table called "followers" which will basically act as a link between two users - a user who is following another user. This kind of relationship is called a Many-to-Many relationship, which means that many users can follow many other users. While creating a relationship, you can select the many-to-many option and choose the tables in strapi itself.
Hi, nice tutorial... I got "Uncaught ReferenceError: qs is not defined" on main.ts. I peeked your github, the "qs" is not there either.. please help? thanks!
If you are using any script file and getting "Uncaught ReferenceError:" which means 'x' is either a variable or a method which you are trying to use before declaring it using var keyword. This means that there is a non-existent variable referenced somewhere. This variable needs to be declared, or you need to make sure it is available in your current script or scope otherwise , it will endup throwing this 'x' is not defined error . This usually indicates that your library is not loaded and JavaScript does not recognize the 'x'.
To solve this error: Load your library at the beginning of all your scripts.
There can be multiple other reasons for this issue:
Path to CDN library you included is not correct
The library file is corrupted
Working offline
Conflict with Other Libraries
You'll need to install the qs package: "npm install qs"
Excellent work 👍
Thank you so much!
The third part is out! dev.to/arnu515/build-an-instagram-...