## Introduction This is part one of this blog series, where we'll learn how to build a YouTube clone. In this part 1, we'll set up the Strapi CMS backend with collections, create data relationships, create custom endpoints for liking, commenting, and viewing videos, set up Socket.io, and create lifecycle methods to listen to real-time updates on Strapi collections. For reference, here's the outline of this blog series: - **Part 1: Building a Video Streaming Backend with Strapi** - Part 2: Creating the App Services and State Management - Part 3: Building the App UI with Flutter ## Prerequisites Before we dive in, ensure you have the following: - [Node](https://nodejs.org/en) and [npm](https://www.npmjs.com/) are installed on your computer. - [Postman](https://www.postman.com/) for API testing. - [Flutter SDK](https://flutter.dev/) installed. - You should be familiar with Flutter and [Strapi CMS CRUD operations](https://docs-next.strapi.io/dev-docs/api/entity-service/crud). You can check out [How to Build a Simple CRUD Application Using Flutter & Strapi](https://strapi.io/blog/how-to-build-a-simple-crud-application-using-flutter-and-strapi). ## Project Structure Below is the folder structure for the app we'll be building throughout this tutorial. ```bash 📦youtube_clone ┣ 📂config: Configuration for the app. ┃ ┣ 📜admin.ts: Admin settings. ┃ ┣ 📜api.ts: API configuration. ┃ ┣ 📜database.ts: Database connection settings. ┃ ┣ 📜middlewares.ts: Middleware configuration. ┃ ┣ 📜plugins.ts: Plugin settings. ┃ ┣ 📜server.ts: Server settings. ┃ ┗ 📜socket.ts: Socket configuration. ┣ 📂database: Database setup files. ┃ ┃ ┣ 📂users-permissions: User permissions config. ┃ ┃ ┃ ┗ 📂content-types: User data structure. ┃ ┣ 📂utils: Utility functions. ┃ ┃ ┗ 📜emitEvent.ts: Event emitter utility. ┃ ┗ 📜index.ts: App's main entry point. ┣ 📂types: TypeScript type definitions. ┃ ┗ 📂generated: Auto-generated type files. ┃ ┃ ┣ 📜components.d.ts: Component types. ┃ ┃ ┗ 📜contentTypes.d.ts: Content type definitions. ┣ 📜.env: Environment variables. ``` ### Why Use Strapi and Flutter? When building a video Streaming app, the developer is required to carefully select the right technologies that will provide an uninterrupted user experience. For the backend, the developer is required to use a scalable infrastructure because chat applications usually have high traffic (different users making concurrent real-time requests), the user base grows faster, and they generate a large amount of data. [Strapi 5](https://strapi.io/five) comes with an effective, scalable, headless CMS that is flexible and easy to use. It allows developers to structure their content, manage their media, and build complex data relationships, eliminating all the overheads from traditional backend development. On the user end, Flutter provides a great solution for easily developing visually pleasant and highly responsive mobile applications. It improves the development of highly performant applications with a different feel and looks for both iOS and Android platforms, supported by its extensive pre-designed widget collection and robust ecosystem. ### Overview of the App We'll Build In this series, we’ll be building a video streaming app that allows users to upload videos, view a feed of videos, and interact with content through likes and comments. The app will feature: - **Authentication**: User authentication will be implemented using Strapi v5 authentication to support user registration, login, and profile management. - **Upload/Streaming Videos**: Users can upload and stream videos to the media library in Strapi v5. - **Real-time Interactions**: Users can comment, post, like, and observe videos updating in real-time by leveraging Socket.IO. - **Video Search and Discovery**: The app allows users to search for videos they want to discover. Below is a demo of what we will build by the end of this blog series.[Link](link) ![final.gif](https://delicate-dawn-ac25646e6d.media.strapiapp.com/final_7cfcf4dfb3.gif) ## Setting Up the Backend with Strapi Let's start by [setting up a Strapi 5 project](https://docs.strapi.io/dev-docs/installation) for our backend. Create a new project by running the command below: ```bash npx create-strapi-app@rc my-project --quickstart ``` The above command will [scaffold a new Strapi 5 project](https://docs.strapi.io/dev-docs/installation) and install the required Node.js dependencies. Strapi uses SQL database as the default database management system. We'll stick with that for the demonstrations in this tutorial. Once the installation is completed, the project will run and automatically open on your browser at `http://localhost:1337`. Now, fill out the form to create your first Strapi administration account and authenticate to the Strapi Admin Panel. ![welcome to strapi.png](https://delicate-dawn-ac25646e6d.media.strapiapp.com/welcome_to_strapi_0f6db59452.png) ## Modeling Data in Strapi Strapi allows you to create and manage your database model from the Admin panel. We'll create a **Video** and **Comment** collections to save the video data and users' comments on videos. To do that, click on the ***Content-Type Builder -> Create new collection type*** tab from your Admin panel to create a **Video** collection for your application and click **Continue**. ![create video collection in Strapi.png](https://delicate-dawn-ac25646e6d.media.strapiapp.com/create_video_collection_in_Strapi_3a2d72cf77.png) Then add the following fields to **Video** collection: | Field | Type | | -------- | -------- | | `title` | Short Text field | | `description` | Long Text field | | `thumbnail` | Single Media field | | `video_file` | Single Media field | Then click the **Save** button. ![video fields .png](https://delicate-dawn-ac25646e6d.media.strapiapp.com/video_fields_9cfe4922ca.png) Next, click **Create new collection type** to create a **Comment** collection and click **Continue**. ![comment collection.png](https://delicate-dawn-ac25646e6d.media.strapiapp.com/comment_collection_91df9220aa.png) Add a `text` field (short text) to the **Comment** collection and click the **Save** button. Lastly, click on **User -> Add another field -> Media** and add a new field named `profile_picture` to allow users to upload their profile pictures when creating an account on the app. ![add media to collection in strapi.png](https://delicate-dawn-ac25646e6d.media.strapiapp.com/add_media_to_collection_in_strapi_6fafd727c0.png) ## Creating Data Relationships I left out some fields in our **Video** and **Comments** collections because they are relations fields. I needed us to cover them separately. For the **Video** Collection, the fields are: - `uploader` - `views` - `comments` - `likes` For the **Comment** collection, the fields are: - `video`(the video commented on) - `user` (the user who posted the comment). ### Adding Relation Field Between Video and Comment Collection Types To add the relation fields to the **Video** collection, click on the **Video -> Add new fields** from **Content-Type Builder** page. Select a Relations from the fields modal, and add a new relation field named `comments`, which will be a [many-to-many relationships](https://strapi.io/blog/understanding-and-using-relations-in-strapi) with the **User** collection. This is so that a user can comment on other users' videos, and another can also comment on their videos. Now click on the **Finish** button to save the changes. ![Select Relation for Video and Comment Collection.png](https://delicate-dawn-ac25646e6d.media.strapiapp.com/Select_Relation_for_Video_and_Comment_Collection_d7072794ea.png) _Select Relation for **Video** and **Comment** Collection_ ![A video can have many comments.png](https://delicate-dawn-ac25646e6d.media.strapiapp.com/A_video_can_have_many_comments_46000424a4.png) _A video can have many comments_ Repeat this process to create the relation field for the `uploader`, `likes`, and `views` fields. Your **Video** collection should look like the screenshot below: ![video collection fields.png](https://delicate-dawn-ac25646e6d.media.strapiapp.com/video_collection_fields_503cfbb545.png) ### Adding Relation Field in Comment Collection For the **Comment** collection, click on the **Comment -> Add new fields** from the **Content-Type Builder** page. Select a Relation from the fields modal and add a new relation field named `user`, which will be a **many-to-many** relationship with the **User** collection. Then click on the **Finish** button to save the changes. ![Create Comment and User relationship.png](https://delicate-dawn-ac25646e6d.media.strapiapp.com/Create_Comment_and_User_relationship_33c80ee4e2.png) _Create Comment and User relationship_ ![A User can have multiple comments Comment Suggest edit Edit from here .png](https://delicate-dawn-ac25646e6d.media.strapiapp.com/A_User_can_have_multiple_comments_Comment_Suggest_edit_Edit_from_here_268cf87306.png) _A User can have multiple comments_ To create the `video` relation field, you must also repeat this process. After the fields, your **Comment** collection will look like the screenshot below: ![comment collection fields.png](https://delicate-dawn-ac25646e6d.media.strapiapp.com/comment_collection_fields_dff99ca746.png) ### Adding New Relation Field to User Collection Now update your **User** Collection to add a new relation field named `subscribers` to save users' video subscribers. Click **User -> Add new fields** from the **Content-Type Builder** page, select the Relation field, and enter `subscribers`, which is also a many-to-many relation with the **User** collection. On the left side, name the field `subscribers`, and on the right, which is the `User` collection, name it `user_subscribers` since they are related to the same collection. ![Adding New Relation Field to User Model.png](https://delicate-dawn-ac25646e6d.media.strapiapp.com/Adding_New_Relation_Field_to_User_Model_203908f616.png) ## Creating Custom Controllers for Like, Views, and Comments. With collections and relationships created, let's [create custom controllers](https://docs.strapi.io/dev-docs/backend-customization/controllers) in our Strapi 5 backend to allow users to like, subscribe, and track users who viewed videos. In your Strapi project, open the `video/controllers/video.ts` file, and extend your Strapi controller to add the like functionality with the code: ```js import { factories } from "@strapi/strapi"; export default factories.createCoreController( "api::video.video", ({ strapi }) => ({ async like(ctx) { try { const { id } = ctx.params; const user = ctx.state.user; if (!user) { return ctx.forbidden("User must be logged in"); } // Fetch the video with its likes const video: any = await strapi.documents("api::video.video").findOne({ documentId: id, populate: ["likes"], }); if (!video) { return ctx.notFound("Video not found"); } // Check if the user has already liked this video const hasAlreadyLiked = video.likes.some((like) => like.id === user.id); let updatedVideo; if (hasAlreadyLiked) { // Remove the user's like video.updatedVideo = await strapi .documents("api::video.video") .update({ documentId: id, data: { likes: video.likes.filter( (like: { documentId: string }) => like.documentId !== user.documentId, ), }, populate: ["likes"], }); } else { // Add the user's like updatedVideo = await strapi.documents("api::video.video").update({ documentId: id, populate:"likes", data: { likes: [...video.likes, user.documentId] as any, }, }); } return ctx.send({ data: updatedVideo, }); } catch (error) { return ctx.internalServerError( "An error occurred while processing your request", ); } }, ); ``` The above code fetches the video the user wants to like and checks if the user has already liked it. If true, it unlikes the video by removing it from the array of likes for that video. Otherwise, it adds a new user object to the array of likes for the video and writes to the database for both cases to update the video records. Then add the code below to the video controller for the views functionality: ```js //.... export default factories.createCoreController( //.... async incrementView(ctx) { try { const { id } = ctx.params; const user = ctx.state.user; if (!user) { return ctx.forbidden("User must be logged in"); } // Fetch the video with its views const video: any = await strapi.documents("api::video.video").findOne({ documentId: id, populate: ["views", "uploader"], }); if (!video) { return ctx.notFound("Video not found"); } // Check if the user is the uploader if (user.id === video.uploader.id) { return ctx.send({ message: "User is the uploader, no view recorded.", }); } // Get the current views const currentViews = video.views.map((view: { documentId: string }) => view.documentId) || []; // Check if the user has already viewed this video const hasAlreadyViewed = currentViews.includes(user.documentId); if (hasAlreadyViewed) { return ctx.send({ message: "User has already viewed this video." }); } // Add user ID to the views array without removing existing views const updatedViews = [...currentViews, user.documentId]; // Update the video with the new views array const updatedVideo: any = await strapi .documents("api::video.video") .update({ documentId: id, data: { views: updatedViews as any, }, }); return ctx.send({ data: updatedVideo }); } catch (error) { console.error("Error in incrementView function:", error); return ctx.internalServerError( "An error occurred while processing your request", ); } }, ); ``` The above code fetches the video clicked by the user by calling the `strapi.service("api::video.video").findOne` method, which checks if the video has been liked by the video before, to avoid a case where a user likes a video twice. If the check is true it will simply send a success message, else it will update the video record to add the user object to the array of likes and write to the database by calling the `strapi.service("api::video.video").update` method. Lastly, add the code to implement the subscribe functionality to allow users to subscribe to channels they find interesting: ```js //.... export default factories.createCoreController( //.... async subscribe(ctx) { try { const { id } = ctx.params; const user = ctx.state.user; if (!user) { return ctx.forbidden("User must be logged in"); } // Fetch the uploader and populate the subscribers relation const uploader = await strapi.db .query("plugin::users-permissions.user") .findOne({ where: { id }, populate: ["subscribers"], }); if (!uploader) { return ctx.notFound("Uploader not found"); } // Check if the user is already subscribed const isSubscribed = uploader.subscribers && uploader.subscribers.some( (subscriber: { id: string }) => subscriber.id === user.id, ); let updatedSubscribers; if (isSubscribed) { // If subscribed, remove the user from the subscribers array updatedSubscribers = uploader.subscribers.filter( (subscriber) => subscriber.id !== user.id, ); } else { // If not subscribed, add the user to the subscribers array updatedSubscribers = [...uploader.subscribers, user.id]; } // Update the uploader with the new subscribers array const updatedUploader = await strapi .query("plugin::users-permissions.user") .update({ where: { id }, data: { subscribers: updatedSubscribers, }, }); return ctx.send({ message: isSubscribed ? "User has been unsubscribed from this uploader." : "User has been subscribed to this uploader.", data: updatedUploader, }); } catch (error) { console.error("Error in subscribe function:", error); return ctx.internalServerError( "An error occurred while processing your request", ); } }, }), ); ``` The above code fetches the details of the channel owner using the Strapi users permission plugin. `plugin::users-permissions.user`. After that, it uses the `strapi.db.query("plugin::users-permissions.user").findOne` method to check if the subscriber user id is present in the subscriber's array, if true, it removes the user from the array of subscribers. Else, it adds the user to the array of subscribers user objects. ## Creating Custom Endpoints for Like, Views, and Comments. Next, create a new file named `custom-video.ts` in the `video/routes` folder and add the following custom endpoints for the controllers we defined earlier: ```js export default { routes: [ { method: 'PUT', path: '/videos/:id/like', handler: 'api::video.video.like', config: { policies: [], middlewares: [], }, }, { method: 'PUT', path: '/videos/:id/increment-view', handler: 'api::video.video.incrementView', config: { policies: [], middlewares: [], }, }, { method: 'PUT', path: '/videos/:id/subscribe', handler: 'api::video.video.subscribe', config: { policies: [], middlewares: [], }, }, ], }; ``` The above code defines custom routes for `like`, `incrementView`, and `subscribe`. The endpoint can be accessed at http://localhost:1337/api/videos/:id/like`, `http://localhost:1337/api/videos/:id/like`, and `http://localhost:1337/api/videos/:id/like`, respectively. ## Configuring Users & Permissions Strapi provides authorization for your collections out of the box, you only need to specify what kind of access you give users. To do this, navigate to **Settings -> Users & Permissions plugin -> Role**. ![Configuring Users & Permissions.png](https://delicate-dawn-ac25646e6d.media.strapiapp.com/Configuring_Users_and_Permissions_c67467c297.png) Here you will find two user roles: - **Authenticated**: A user with this role will have to be authenticated to perform some certain roles. Users in this category normally receive more enabled functionality than users in the Public category. - **Public**: This role is assigned to users who are not logged in or authenticated. For the **Authenticated** role, give the following access to the collections: | Collection | Access | | -------- | -------- | | **Comments** | `find`, `create`, `findOne` and `update` | | **Videos** | `create`, `incrementView`, `subscribe`, `update`, `find`, `findOne`, and `like ` | | **Upload** | `upload` | | Users-permissions(User) | `find`, `findOne`, `update`, `me` | Then give the **Public** role the following access to the collections: | Collection | Access | | -------- | -------- | | **Comments** | `find` and `findOne` | | **Videos** | `find` and `findOne` | | **Upload** | `upload` | | **Users-permissions (User)** | `find` and `findOne` | The above configurations allow the **Public** role (unauthenticated user) to view videos and the details of the user who uploaded the video, such as username, profile picture, and number of subscribers. We also gave it access to see comments on videos and upload files because users must upload a profile during sign-up. For the Authenticated role, we gave it more access to comment, like, subscribe, and update their user details. ## Implementing Real-time Features with Socket.IO We need to allow users to get real-time updates when a new video, comment, or like is created or when a video is updated. To do this, we'll use [Socket.IO](https://socket.io/). We'll write a custom Socket implementation in our Strapi project to handle real-time functionalities. First, install Socket.IO in your Strapi project by running the command below: ```bash npm install socket.io ``` Then, create a new folder named `socket` in the `api` directory for the socket API. In the `api/socket` directory, create a new folder named `services` and a `socket.ts` file in the `services` folder. Add the code snippets below to setup and initialize a socket connection: ```js import { Core } from "@strapi/strapi"; export default ({ strapi }: { strapi: Core.Strapi }) => ({ initialize() { strapi.eventHub.on('socket.ready', async () => { const io = (strapi as any).io; if (!io) { strapi.log.error("Socket.IO is not initialized"); return; } io.on("connection", (socket: any) => { strapi.log.info(`New client connected with id ${socket.id}`); socket.on("disconnect", () => { strapi.log.info(`Client disconnected with id ${socket.id}`); }); }); strapi.log.info("Socket service initialized successfully"); }); }, emit(event: string, data: any) { const io = (strapi as any).io; if (io) { io.emit(event, data); } else { strapi.log.warn("Attempted to emit event before Socket.IO was ready"); } }, }); ``` Then update your `src/index.ts` file to initialize the Socket.IO server, set up event listeners for user updates and creations, and integrate the socket service with Strapi's [lifecycle hooks](https://docs.strapi.io/dev-docs/backend-customization/models): ```js import { Core } from "@strapi/strapi"; import { Server as SocketServer } from "socket.io"; import { emitEvent, AfterCreateEvent } from "./utils/emitEvent"; interface SocketConfig { cors: { origin: string | string[]; methods: string[]; }; } export default { register({ strapi }: { strapi: Core.Strapi }) { const socketConfig = strapi.config.get("socket.config") as SocketConfig; if (!socketConfig) { strapi.log.error("Invalid Socket.IO configuration"); return; } strapi.server.httpServer.on("listening", () => { const io = new SocketServer(strapi.server.httpServer, { cors: socketConfig.cors, }); (strapi as any).io = io; strapi.eventHub.emit("socket.ready"); }); }, bootstrap({ strapi }: { strapi: Core.Strapi }) { const socketService = strapi.service("api::socket.socket") as { initialize: () => void; }; if (socketService && typeof socketService.initialize === "function") { socketService.initialize(); } else { strapi.log.error("Socket service or initialize method not found"); } }, }; ``` The above code sets up the Socket.IO configuration, creates a new `SocketServer` instance when the HTTP server starts listening, and subscribes to database lifecycle events for **User** collection to emit real-time updates. Next, create a new folder named `utils` in the `src` folder. In the `utils` folder, create an `emitEvents.ts` file and add the code snippets below to define an `emitEvent` function: ```js import type { Core } from "@strapi/strapi"; interface AfterCreateEvent { result: any; } function emitEvent(eventName: string, event: AfterCreateEvent) { const { result } = event; const strapi = global.strapi as Core.Strapi; const socketService = strapi.service("api::socket.socket"); if (socketService && typeof (socketService as any).emit === "function") { (socketService as any).emit(eventName, result); } else { strapi.log.error("Socket service or emit method not found"); } } export { emitEvent, AfterCreateEvent }; ``` This function emits socket events when certain database actions occur. It takes an event name and an `AfterCreateEvent` object as parameters, extracts the result from the event, and uses the socket service to emit the event with the result data. ### Creating Lifecycles methods for Video Collection Now let's use the lifecycle method for the Video and Comment collection methods to listen to create, update, and delete events. Create a `lifecycles.ts` file in the `api/video/content-type/video` folder and add the code below: ```js import { emitEvent, AfterCreateEvent } from "../../../../utils/emitEvent"; export default { async afterUpdate(event: AfterCreateEvent) { emitEvent("video.updated", event); }, async afterCreate(event: AfterCreateEvent) { emitEvent("video.created", event); }, async afterDelete(event: AfterCreateEvent) { emitEvent("video.deleted", event); }, }; ``` ### Creating Lifecycles methods for Comment Collection Next, create a `lifecycles.ts` file in the `api/comment/content-type/comment` folder and add the code below: ```js import { emitEvent, AfterCreateEvent } from "../../../../utils/emitEvent"; export default { async afterCreate(event: AfterCreateEvent) { emitEvent("comment.created", event); }, async afterUpdate(event: AfterCreateEvent) { emitEvent("comment.updated", event); }, async afterDelete(event: AfterCreateEvent) { emitEvent("comment.deleted", event); }, }; ``` Lastly, update the `bootstrap` function in your `src/index.ts` file to listen to create and update events in the `users-permissions.user` plugin: ```js // ... bootstrap({ strapi }: { strapi: Core.Strapi }) { //... strapi.db.lifecycles.subscribe({ models: ["plugin::users-permissions.user"], async afterUpdate(event) { emitEvent("user.updated", event as AfterCreateEvent); }, async afterCreate(event) { emitEvent("user.created", event as AfterCreateEvent); }, }); }, ``` ## Testing Events and Real Time Updates with Postman To test this out, open a new Postman Socket.io window. ![test on postman.png](https://delicate-dawn-ac25646e6d.media.strapiapp.com/test_on_postman_4126dc5628.png) ### Enable Events Then, connect to your Strapi backend by entering the Strapi API URL. Click the **Events** tab and enter the lifecycle events we created in the Strapi backend. Check the **Listen** boxes for all the events you want to monitor, and click the **Connect** button. ![enable events on postman.png](https://delicate-dawn-ac25646e6d.media.strapiapp.com/enable_events_on_postman_cddd1aef7b.png) ### Create Entries to See Events Emitted Now return to your Strapi Admin panel, navigate to ***Content Manager -> Video -> + Create new entries***, and create new video entries. ![create video entry.png](https://delicate-dawn-ac25646e6d.media.strapiapp.com/create_video_entry_a81f5aebdf.png) Once you perform actions in your Strapi admin panel that trigger the lifecycle events you've set, such as creating, updating, and deleting your collections, you should see notifications show up in Postman. This will enable you to verify that your Strapi backend emits events and that your WebSocket connection functions as expected. ![event emitted.png](https://delicate-dawn-ac25646e6d.media.strapiapp.com/event_emitted_1f01d4429f.png) We're done with part one of this blog series. Stay tuned for Part 2, where we'll continue this tutorial by building the frontend with Flutter and consuming the APIs to implement a functional YouTube clone application. The code for this Strapi backend is available on my [Github repository](https://github.com/icode247/youtube_clone_with_flutter_and_strapi). We've split the tutorial into three parts, each in its own branch for easier navigation. The ***main*** branch contains the Strapi code. The ***part_2*** branch holds the Flutter code for state management and app services, but if you run it, you'll only see the default Flutter app since these logics are connected to the UI in ***part_3***. The ***part_3*** branch contains the full Flutter code with both the UI and logic integrated. ## Conclusion In part one of this tutorial series, we learned how to set up the Strapi backend with collections, create data relationships, create custom endpoints for liking, commenting, and viewing videos, set up Socket.io, and create lifecycle methods to listen to real-time updates on the collections. In the next part, we will learn how to build the frontend with Flutter.