Build a Full-stack App Video Collections Using NextJs, Apollo.io Client, Graphql, Mysql, Typeorm and ExpressJs
Published: 2023-09-06 12:13:56


![Image 1, Tech Stack Bayu Syaits](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/spmtyowwymmr0hc7aheo.png)

## Introduction

In this article I'm going to explain the code on the Video Collections app that I've created. It's a way for me to better understand the code that I have created and a way to exchange ideas with my developer friends. Before discussing technical issues, I'm going to list some of the features available in this app, including:

1. **Video List:** Shows the entire video, at least 10 videos displayed. If there is more than that user can click Load More to add 10 videos to display. The Video Card must contain: Checkbox, title, image, short description, collections and categories. If the card is clicked, it can lead to the detail page. If the checkbox is on the `Add to Collection` text on the active Sidebar.

2. **Add to Collection:** This button will enable if the user has selected several videos and can `add to collection`. If the user clicks on this button, it will display a popup containing the `list collection` and the button Create or `Submit Collection`. If a user does not have a `list collection`, the user can create a new collection and will be directed to the popup `create collection`. After the user selects the collection and `submit`, the video will display the collection list on his card. 
3. **Create Collection:** Displays popups containing form `title` and `link image`. After submitting, the collection will be added to the database.
4. **Video Details:** Displays `rank`, `rates`, `episodes`, `collections`, `categories`, `release dates` and `video descriptions`. In addition, users can add videos to the collection by clicking `Add to Collection` and popup.


I made this project using: 
1. **NextJs:** I chose to build this application using `NextJs`, because it's quick and easy to set up.
2. **Graphql:** Developed by the Facebook team in 2012, aims to get a more flexible and efficient API. GraphQL itself is built on three main foundations: `query`, `resolver`, and `schema`. Using GraphSQL, developers can call the data they need through a query. 
3. **Apollo:** To handle requests from the client side when using GraphQL, we can use Apollo servers.
4. **Mysql:** Since I am quite familiar with SQL query and supports various types of data types, I chose to implement MYSQL in this project.
5. **Typeorm:** TypeORM makes it easy to build an ORM-based application using an object-oriented programming paradigm.
6. **ExpressJs:** I need a flexible framework but also a scalable one, for which I'm using `expressJs` that's pretty popular with NodeJs users.

## Implementation API

The first folder I'm going to discuss is the api folder. It contains entities, and services whose names represent the table name, namely `category`, `collection`, `user`, `video`, `video-category` and `video-collections`. Each of these folders contains `typeDefs.ts`, `args.ts`, `entity.ts` and `resolvers.ts` files. I'll explain briefly about the files.

The first is `typeDefs.ts`, which contains data forms that are defined in the GraphQL schema language. The schema itself consists of one or more types such as Object Type, Scalar Type, Query Type, Mutation Type and Input Type. Here's a piece of code on the typeDEFS file in the video folder.

```
export const typeDefs = `
  extend type Query {
    getVideo(slug: String!): Video
  }
  
  extend type Mutation {
    addVideo(payload: PayloadAddVideo): Video!
  }

  type Gallery {
    image: String
  }

  input PayloadAddVideo {
    title: String!
    image: String!
    episode: Int
    description: String
    isCencor: Int
    rates: Int
    rank: Int
    type: String
  }

  type Video {
    uuid: String!
    title: String!
    slug: String!
    image: String
    gallery: [Gallery]
    videoCollections: [VideoCollection]
    videoCategories: [VideoCategory]
  }
`;

```

The Apollo server needs to know how to fill in the data in the schema in order to be able to respond to the client's request. To this, a resolution is required, following this piece of code in the file `resolver.ts` on the video folder.


```
import { isEmpty } from "lodash";
import { v4 } from "uuid"
import Args, { VideoResponse } from "./args";
import { AppDataSource } from "../../data-source"
import { Video as VideoEntity } from "./entity";
import { filterItems } from "../../helpers/filter";
import { generateKey, setSpaceToDash } from "../../helpers/mixins";
import slugify from "../../helpers/slugify";

// Provide resolver functions for your schema fields
export const Query = {
  getVideo: async (_: any, args: any) => {
    const { slug } = args;
    if (!slug) {
      return null;
    }
    const videoEntity = AppDataSource.getRepository(VideoEntity)
    return await videoEntity.findOne({ 
      relations: {
        videoCollections: true,
        videoCategories: true
      },
      where: { 
        slug: setSpaceToDash(slug)
      } 
    });
  }
}

export const Mutation = {
  addVideo: async (_: any, args: any) => {
    try {
      const generate = Math.floor(generateKey(100))
      const { 
        payload: {
          title, image, episode, rates, rank,
          status, description, gallery
        }
      } = args;
      const video = new VideoEntity()
      video.uuid = v4()
      if (episode) {
        video.episode = episode
      }
      if (rates) {
        video.rates = rates
      }
      if (rank) {
        video.rank = rank
      }
      if (description) {
        video.description = description
      }
      if (status && ['editorial','favorite'].includes(status)) {
        video.status = status
      }
      if (gallery && Array.isArray(gallery)) {
        video.gallery = gallery
      }
      video.slug = slugify(title) ? `${setSpaceToDash(slugify(title))}_${generate}` : 
      `${setSpaceToDash(title)}_${generate}`
      video.image = image
      const videoRepository = AppDataSource.getRepository(VideoEntity)
      return await videoRepository.save(video);
    } catch (error) {
      return {};
    }
  }
}

```

The next file is `args.ts`, I called the `type-graphql` library to use some decorators like `Argstype`, Field and `ObjectType` to define GraphQL schemes easily.
We can associate `Class Args` properties with fields (limit, offset, type, dst) in a GraphQL scheme using Fields. Whereas to define the argument type in the `resolver` of a class, we can use Argstype and to associate the object type can use `ObjectType`. Here's a piece of code on the `args.ts` file in the video folder:

```
import { Field, ArgsType, ObjectType } from 'type-graphql';
@ArgsType()
export default class Args {
  @Field({ defaultValue: 10 })
  limit: number;

  @Field({ defaultValue: 0 })
  offset: number;

  @Field({ nullable: true })
  type?: string;

  @Field({ nullable: true })
  sortBy?: string;

  @Field({ nullable: true })
  search?: string;

  @Field({ nullable: true })
  slug?: string;
}

@ObjectType()
export class Gallery {
  @Field()
  image: string;
}

```

Each typeDefs.ts and `resolvers.ts` file will be linked via `server.app.ts`, below is an example of how to connect `User` and `Category` services with `apollo-server-express` and integrate them into `ExpressJs`.


```
import "reflect-metadata";
import * as express from "express";
import { ApolloServer, makeExecutableSchema } from "apollo-server-express";

import { merge } from "lodash";
import { typeDefs as UserTypeDefs } from "./app/services/user/typeDefs";
import { typeDefs as CategoryTypeDefs } from "./app/services/category/typeDefs";
import {
   Query as UserQuery,
   Mutation as UserMutations
} from "./app/services/user/resolvers";
import {
  Query as CategoryQuery,
  Mutation as CategoryMutations
} from "./app/services/category/resolvers";
const startServer = async () => {
  const schema = makeExecutableSchema({
    typeDefs: [ 
      CategoryTypeDefs,
      UserTypeDefs,
      VideoTypeDefs,
      CollectionTypeDefs,
      VideoCollectionTypeDefs,
      VideoCategoryTypeDefs
    ],
    resolvers: merge(
      {
        Query: {
          ...CategoryQuery,
          ...UserQuery,
        },
        Mutation: {
          ...CategoryMutations,
          ...UserMutations,
        }
      }
    ),
  });
  const server = new ApolloServer({ 
    schema,
    introspection: true,
    playground: true,
    tracing: true,
  });

  const app: express.Application = express();

  server.applyMiddleware({ app });

  app.listen({ port: PORT }, () =>
    console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`)
  );
};

startServer();
```

The last part is the `entity.ts` file, in this project I use a typeorm which is a representation of a table in a relational database. By defining a class in this Entity file, TypeORM will automatically create a table with a schema defined in the Video class according to the decorator defined on each column, and we can use TypeORN to perform CRUD operations without having to write SQL queries directly.

```
import { 
  Entity, 
  Column, 
  PrimaryColumn,
  Generated,
  BaseEntity,
  OneToMany,
  JoinColumn,
  DeleteDateColumn,
  CreateDateColumn,
  UpdateDateColumn,
} from "typeorm";

import { VideoCollection } from "../video-collection/entity";
import { VideoCategory } from "../video-category/entity";

export enum Status {
  EDITORIAL = "editorial",
  FAVORITE = "favorite",
}

type Gallery = {
  image: string
}

@Entity()
export class Video extends BaseEntity {
  @PrimaryColumn("char", {length: 100})
  @Generated("uuid")
  uuid: string

  @Column("char", { length: 50 })
  title: string;

  @OneToMany(() => VideoCollection, 
    (videoCollection) => videoCollection.videoUuid,
    {
      cascade: ["insert", "update"],
    })
  @JoinColumn([
      { name: "uuid" }
  ])
  public videoCollections: VideoCollection[]

  @OneToMany(() => VideoCategory, 
    (videoCategory) => videoCategory.videoUuid,
    {
      cascade: ["insert", "update"],
    })
  @JoinColumn([
      { name: "uuid" }
  ])

  public videoCategories: VideoCategory[]

  @Column("text", {nullable: true})
  description: string;

  @Column({ type: "int", nullable: true })
  episode: number;

  @Column({ type: "int", nullable: true })
  rates: number;

  @Column({ type: "int", nullable: true })
  rank: number;

  @Column({ type: "int", default: 0 })
  isCencor: number;

  @Column("char", { length: 20, nullable: true })
  type: string;

  @Column({ type: "simple-json", default: "", nullable: true })
  gallery: Gallery[];
  
  @Column({default: null, nullable: true})
  image: string;

  @Column("char", { length: 60 })
  slug: string;

  @Column({
    type: "enum",
    enum: Status,
    default: Status.FAVORITE
  })
  status: Status

  @Column({ type: 'date', nullable: true })
  publishDate: Date;

  @CreateDateColumn()
  createdDate: Date;

  @UpdateDateColumn()
  updateDate: Date;
  
  @DeleteDateColumn()
  deletedDate: Date;
}
```

All entity files in each service folder are connected using DataSource. On the code below is an example of the dataSource configuration to connect to the `MySQL` database, in the `data-source.ts` file.

```
import "reflect-metadata"
import { DataSource } from "typeorm"
import { User } from "./services/user/entity"
import { Category } from "./services/category/entity"
import { Video } from "./services/video/entity"
import { Collection } from "./services/collection/entity"
import { VideoCollection } from "./services/video-collection/entity"
import { VideoCategory } from "./services/video-category/entity"

export const AppDataSource = new DataSource({
    type: "mysql",
    host: "127.0.0.1",
    port: 3306,
    username: "root",
    password: "xxxxx",
    database: "db_collections",
    synchronize: true,
    logging: false,
    entities: [
        User, 
        Category, 
        Video, 
        Collection, 
        VideoCollection,
        VideoCategory
    ],
    migrations: [],
    subscribers: [],
})
AppDataSource.initialize()
    .catch((error) => console.log(error))

```

## Implementation APP

This app is built using `NextJs`, I just follow the documentation and use the features available on this framework. However, I've added some libraries like `material-ui`, `GraphQL`, `apollo` and `react-hook-form` to speed up the development process and adapt to the needs of this project.
If in the api folder I have described the use of Apollo and GraphQL on the server side, then in this app folder I will describe the use on the client side. First, on the `_app.tsx` file we need to configure and redirect to the `GraphQL` server endpoint that has been created in the api folder. Then we can integrate the `Apollo Client` with the React application using `ApolloProvider` and connect it to the function `client`. For more information, you can see the code examples below:

```
import type { AppProps } from "next/app";
import { useEffect, useState } from "react";
import { unregister } from "next-offline/runtime";
import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import theme from "hooks/theme";
import { sha256 } from 'crypto-hash';

import Offline from "./offline";
import Layout from "base-components/layout";
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
import { createHttpLink } from 'apollo-link-http';
import { REACT_APP_API_URL } from "config";
import { AppBar, Grid, Toolbar } from "@mui/material";
import Typography from '@mui/material/Typography';
import { Box, Container } from "@mui/system";
import Link from "next/link";

const httpLink = createPersistedQueryLink({ sha256 }).concat(
  new createHttpLink({ uri: REACT_APP_API_URL }),
);

const client = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache(),
});

export default function MyApp({
  Component,
  emotionCache = clientSideEmotionCache,
  pageProps,
}: AppProps & {
  emotionCache: any;
}) {
  const [isDisconnected, setDisconnected] = useState<boolean>(false);
  const handleConnectionChange = () => {
    if (!navigator.onLine) {
      setDisconnected(true);
    } else {
      setDisconnected(false);
    }
  };
  useEffect(() => {
    unregister();
    const style: any = document.getElementById("server-side-styles");
    if (style) {
      style.parentNode.removeChild(style);
    }
    window.addEventListener("offline", handleConnectionChange);
    window.addEventListener("online", handleConnectionChange);
  });
  const setComponent = () => {
    let d: any = <Component {...pageProps} />;
    if (isDisconnected) {
      d = <Offline />;
    }
    return d;
  };
  return (
    <ApolloProvider client={client as any}>
        <Layout>
          <ThemeProvider theme={theme}>
            <CssBaseline />
            <AppBar position="relative">
              <Toolbar
                sx={{
                  pr: '24px',
                }}
              >
                <Link 
                  style={{
                    textDecoration: 'none'
                  }}
                  href="/">
                  <Typography
                    component="h1"
                    variant="h6"
                    color="inherit"
                    noWrap
                    sx={{ 
                      flexGrow: 1, 
                      color: 'white'
                    }}
                  >
                      Video List
                  </Typography>
                </Link>
              </Toolbar>
            </AppBar>
            <Box>
              <Container maxWidth="lg" sx={{ px: '24px', mt: 4, mb: 4 }}>
                <Grid container spacing={1}>
                  {setComponent()}
                </Grid>
              </Container>
            </Box>
          </ThemeProvider>
        </Layout>
    </ApolloProvider>
  );
}

```

## Implementation Query & Mutation

After config, the most common step is to use the `Apollo Client` with a query and a `GraphQL` mutation on each component. Below is an example in the `queries.ts` file in the `app folder > components > video > details`.

In the `GET_VIDEO` query, I have called the `JoinTable` column (`videoCategories` dan `videoCollections`). When dealing with this, we can implement resolvers to call field `joinTable` TypeORM through GraphQL. In this case, I take the value uuid in the column `videoCategories` and `videoCollections`. The rest of the values that I'm calling adjust to the needs in each column.

In the `PUT_BULK_VIDEO_COLLECTION` mutation section, I call `PayloadBulkVideoCollect` which is the Input Type. This Input type is called in a mutation to define the type of data that can be sent as an argument in the `bulkVideoCollection` operation to be sent to the server. These Input Types are very useful in `GraphQL`, as they allow developers to organize arguments well.

```
import gql from "graphql-tag";
export const PUT_BULK_VIDEO_COLLECTION = gql`
  mutation bulkVideoCollection(
    $payload: [PayloadBulkVideoCollection!]!
  ) {
    bulkVideoCollection(
      payload: $payload
    ) {
      uuid
      userUuid
    }
  }
`;
export const DELETE_VIDEO_COLLECTION = gql`
  mutation deleteVideoCollection(
    $uuid: String!
    $userUuid: String!
  ) {
    deleteVideoCollection(
      uuid: $uuid
      userUuid: $userUuid
    )
  }
`;
export const GET_VIDEO = gql`
  query getVideo(
    $slug: String!
  ) {
    getVideo(
      slug: $slug
    ) {
      uuid
      title
      slug
      episode
      description
      type
      image
      videoCategories {
        id
        uuid
        categoryUuid {
          title
        }
      }
      videoCollections {
        uuid
        collectionUuid {
          title
          uuid
        }
        id
      }
    }
  }
`;
```

## Implementation useQuery dan useMutation

There are two hooks provided by the `Apollo Client` library, `useQuery` and `useMutation`. Below is the code in the `components > video > details > index.tsx`. One of the advantages of using an `apollo client` is that developers can control the data taken from the cache and/ or from the server by adding the `fetchPolicy` option.

Values for `fetchPolicy` options are numerous, including: `cache-first`, `cache-and-network`, `network-only`, `cache`, `no-catch` and `standby`. For an explanation of this value, you can read on this [link](https://www.apollographql.com/docs/react/data/queries/).

In addition, when using `useQuery` we can also deal with loading and errors that may occur during requests to the server, or perform `refetch` (method used to reload data from the server with GraphQL query) if successful request on `useMutation`.

The next hook is `useMutation`, in the code below I call the mutation `DELETE_VIDEO_COLLECTION`. Mutations in GraphQL are used to send data to servers to update data, just as `useQuery` in `useMutation` can also handle `loading`, `error` during request to the server and retrieve `data`. 

`deleteVideoCollection` is a function called to run a mutation. for the payload/ parameter sent as an argument, we can put it in the variables. If the change of `data`/ `mutation` is successful then it will reload the data in the `GET_VIDEO` query by adding the option `onCompleted: refetch`.


```
import React, { useContext } from "react";
import { useQuery, useMutation } from '@apollo/client';
import VideoDetailView from "./VideoDetailView";
import VideoDetailSidebarView from "./VideoDetailSidebarView";
import VideoDetailModal from "./modal";
import VideoDetailModalCreate from "./modal-create";
import { useModal, ModalPopupDispatchContext } from "hoc/withModal";
import { useRouter } from "next/router";
import { Grid } from "@mui/material";
import { debounce } from "lodash";
import { GET_VIDEO, DELETE_VIDEO_COLLECTION } from './queries'

type VideoProps = {};
const VideoDetailContainer: React.FC<VideoProps> = () => {
  const router = useRouter();
  const {
    query: {
      slug
    }
  }: any = router
  const { loading, error, data, refetch } = useQuery(GET_VIDEO, {
    fetchPolicy: "cache-and-network",
    variables: {
      slug
    },
  }) 
  const { openModal } = useModal();
  const { closeModal, onSubmitModal } = useContext(ModalPopupDispatchContext);
  const [deleteVideoCollection, {}] = useMutation(DELETE_VIDEO_COLLECTION, {
    onCompleted: refetch,
    awaitRefetchQueries: true
  });
  const handleRemoveCollection = (val: string)  => () => {
    deleteVideoCollection({
      variables: {
          uuid: val,
          userUuid: 'de4e31bd-393d-40f7-86ae-ce8e25d81b00'
        } 
      },
    )
    .catch((err: any) => {
      console.log('[011] err', err)
    });
  }
  const openModalAddCollection = debounce(() => {
    const onFinish = () => {
      onSubmitModal();
    };
    const onSwitch = () => {
      closeModal();
      openModalCreateCollection()
    }
    openModal({
      title: "Add to Collection",
      hideClose: false,
      component: () => (
        <VideoDetailModal
          onFinish={onFinish}
          onSwitch={onSwitch}
          field={{...data.getVideo}}
        />
      ),
      onClose: () => {
        closeModal();
      },
    });
  }, 1000);
  const openModalCreateCollection = debounce(() => {
    const onFinish = () => {
      onSubmitModal();
      openModalAddCollection()
    };
    const onSwitch = () => {
      closeModal();
      openModalAddCollection()
    }
    openModal({
      title: "Create Collection",
      hideClose: false,
      component: () => (
        <VideoDetailModalCreate
          onFinish={onFinish}
          onSwitch={onSwitch}
        />
      ),
      onClose: () => {
        closeModal();
      },
    });
  }, 1000);
  const handleAddCollection = (e: React.FormEvent<HTMLFormElement>) => {
    if (data?.getVideo) {
      openModalAddCollection()
    }
  }
  const handlerList = {
    error,
    loading,
    data,
    handleRemoveCollection
  }

  const handlerCollection = {
    handleAddCollection,
    error,
    loading,
    data
  }

  return (
    <Grid 
        container 
        rowSpacing={3} 
        columnSpacing={{ xs: 2, sm: 2, md: 2, lg: 2 }}
        justifyContent="start"
        alignItems="start"
      >
      <Grid item lg={3} xl={3} xs={12} sm={12} md={3}>
        <VideoDetailSidebarView {...handlerCollection} />
      </Grid>
      <Grid item lg={9} xl={9} xs={12} sm={12} md={9}>
        <VideoDetailView {...handlerList} />
      </Grid>
    </Grid>
  );
}

export default VideoDetailContainer
```

## File Query SQL

The last folder in this project, `db_collections`, contains SQL queries to create tables and mock data. This is to make it easier for friends who want to try this app without having to make data samples from scratch.

## Conclution

GraphQL can be an alternative if friends want to build applications instead of using REST-API. One of its advantages is that developers can call the required data through a query and I also use apollo to handle requests from the client side. The library that I use in this project is very helpful in the development process in addition to the flexibility and scalability factors, the project becomes faster in developing with the library-library that I have explained above.

If there are errors or there is a way/ technique that friends think is better when implementing the above material, please comment below or you can send a message to me on [linkedIn](https://www.linkedin.com/in/bayusyaits/). For friends who want to learn more and explore, please check my [github](https://github.com/Bayusyaits/video-collections-next). Once you clone, and install depedency in each folder (api/ app), you can run by following the commands in the scripts in each `packages.json`.