---
title: "A Full-Stack Web App Using Angular and GraphQL: Data Fetching and Manipulation (Part 2)"
published_at: "2024-01-10T10:31:00+00:00"
modified_at: "2026-01-09T13:11:02+00:00"
url: "https://www.syncfusion.com/blogs/post/full-stack-app-angular-graphql-2"
excerpt: "In this blog, we’ll learn to fetch and manipulate data using GraphQL queries and mutations in a full-stack Angular app."
taxonomy_category:
  - "Angular"
  - "Development"
  - "Syncfusion"
  - "UI"
  - "Web"
taxonomy_post_tag:
  - "Angular"
  - "GraphQL"
  - "productivity"
  - "Syncfusion"
  - "Web Development"
---

# A Full-Stack Web App Using Angular and GraphQL: Data Fetching and Manipulation (Part 2)

[Ankit Sharma](https://www.syncfusion.com/blogs/author/ankit-sharma)

![A Full-Stack Web App Using Angular and GraphQL](https://www.syncfusion.com/blogs/wp-content/uploads/2024/01/A-Full-Stack-Web-App-Using-Angular-and-GraphQL.png)


Welcome to the second part of our series on building a full-stack web application using Angular and GraphQL. In [part 1](https://www.syncfusion.com/blogs/post/full-stack-web-app-angular-graphql.aspx)
, we learned how to configure a full-stack .NET app to create a GraphQL server using Hot Chocolate. We created a GraphQL query and used the Banana Cake Pop IDE to execute it.

In this installment, we will configure the Angular client using Apollo GraphQL to consume the GraphQL server endpoints. We will also create a GraphQL mutation to add new movie data. This article is designed to guide you through integrating GraphQL with Angular, demonstrating how data can be fetched and manipulated using GraphQL queries and mutations.

So, let’s continue our journey into the fascinating world of full-stack development with Angular and GraphQL!

## Configure the app to handle the uploaded poster images

We’ll design the app to allow the users to upload the movies’ poster images while adding movie data. We will store the images on the server inside a folder named **Poster.** To do this, add the following code to the ** Program.cs**file.

```
var FileProviderPath = app.Environment.WebRootPath + "/Poster";
if (!Directory.Exists(FileProviderPath))
{
    Directory.CreateDirectory(FileProviderPath);
}
app.UseFileServer(new FileServerOptions
{
    FileProvider = new PhysicalFileProvider(FileProviderPath),
    RequestPath = "/Poster",
    EnableDirectoryBrowsing = true
});
```

In this process, we’re setting up a file server to deliver static files from a designated directory. The variable **FileProviderPath** is assigned the directory path containing the static files. If the directory doesn’t exist, it will be created using the ** Directory.CreateDirectory** method.

The **app.UseFileServer** method is invoked to set up the file server, with a ** FileServerOptions** object passed as a parameter to define the server’s settings. The ** FileProvider** property is assigned a ** PhysicalFileProvider** object that targets the ** FileProviderPath** directory. The ** RequestPath** property is set to **“/Poster”**, denoting the URL path from which the server will deliver files. The ** EnableDirectoryBrowsing** property is enabled, allowing users to view the directory’s contents.

A default image is displayed if a user doesn’t upload a movie poster. The name of this default image is added to the **appsettings.json** file, as shown.

```
"DefaultPoster": "DefaultPoster.jpg",
```

**Note:** The default image hasn’t been added to the app yet. We’ve only included its name in the **appsettings.json** file for consistency. The default poster image will be incorporated once the ** Poster** folder is created during the first execution of the app. We’ll cover this process later in this article.

## Update the IMovie interface

Update the **IMovie.cs** file by adding the method declaration, as shown.

```
public interface IMovie
{
    // other methods.
    
    Task AddMovie(Movie movie);
}
```

## Implement the Add Movie functionality in the data access layer

Since we have added a new method in the **IMovie** interface, we need to update the ** MovieDataAccessLayer** class by implementing the method. Refer to the following code example.

```
public async Task AddMovie(Movie movie)
{
    try
    {
        await _dbContext.Movies.AddAsync(movie);
        await _dbContext.SaveChangesAsync();
    }
    catch
    {
        throw;
    }
}
```

In this method, the **AddAsync** method is called on the**_dbContext.Movies** object, passing in the movie object as a parameter. This adds the movie object to the ** Movies** table in the database. The ** SaveChangesAsync** method is then called on the **_dbContext** object to save the changes to the database.

If an exception occurs during the process, the catch block catches the exception and rethrows it. This helps with error handling, ensuring that any exceptions that occur are not left unhandled, which could lead to crashes.

## Add a GraphQL mutation resolver

A GraphQL mutation is used to mutate or change the data on the server. Add a class called **MovieMutationResolver.cs** inside the ** MovieApp\GraphQL** folder and then add the following code inside it.

```
using MovieApp.Interfaces;
using MovieApp.Models;
namespace MovieAppDemo.GraphQL
{
  public class MovieMutationResolver
  {
    public record AddMoviePayload(Movie movie);
    readonly IWebHostEnvironment _hostingEnvironment;
    readonly IMovie _movieService;
    readonly IConfiguration _config;
    readonly string posterFolderPath = string.Empty;
    public MovieMutationResolver(IConfiguration config, IMovie movieService, IWebHostEnvironment hostingEnvironment)
    {
        _config = config;
        _movieService = movieService;
        _hostingEnvironment = hostingEnvironment;
        posterFolderPath = System.IO.Path.Combine(_hostingEnvironment.WebRootPath, "Poster");
    }
    [GraphQLDescription("Add a new movie data.")]
    public AddMoviePayload AddMovie(Movie movie)
    {
        if (!string.IsNullOrEmpty(movie.PosterPath))
        {
            movie.PosterPath = WriteImageToServer(movie);
        }
        else
        {
            movie.PosterPath = _config["DefaultPoster"];
        }
        _movieService.AddMovie(movie);
        return new AddMoviePayload(movie);
    }
    string WriteImageToServer(Movie movie)
    {
        string fileName = Guid.NewGuid() + ".jpg";
        string fullPath = System.IO.Path.Combine(posterFolderPath, fileName);
        byte[] imageBytes = Convert.FromBase64String(movie.PosterPath);
        File.WriteAllBytes(fullPath, imageBytes);
        return fileName;
    }
  }
}
```

In the **MovieMutationResolver** class, we’ve initialized the **_config**, **_movieService,** and **_hostingEnvironment** objects in the constructor. We’ve also set up the ** posterFolderPath** variable, which points to the Poster directory in the web root path.

Next, we have the **AddMovie** method. This method accepts a ** Movie** object and returns an ** AddMoviePayload** object. It begins by checking whether the ** PosterPath** property of the ** Movie** object is null or empty. If not, it uses the ** WriteImageToServer** method to save the image on the server and updates the ** PosterPath** property with the file name.

The **AddMovie** method sets the ** PosterPath** property to the default poster path from the configuration file if empty. Then, it uses the ** AddMovie** method of the **_movieService** object to add the ** Movie** object to the database. Finally, it returns a new ** AddMoviePayload** object containing the added ** Movie** object.

The **WriteImageToServer** method accepts a ** Movie** object and returns the file name of the saved image. It creates a unique file name using the ** Guid.NewGuid()** method and saves the image on the server with the ** File.WriteAllBytes** method.

## Register mutation resolver

Let’s enhance the registration of the mutation resolver. Update the **Program.cs** file as follows.

```
builder.Services.AddGraphQLServer()
    .AddQueryType<MovieQueryResolver>() 
    .AddMutationType<MovieMutationResolver>();
```

This code adds the **MovieQueryResolver** and ** MovieMutationResolver** to the GraphQL server, enabling it to handle queries and mutations for movie data.

## Configure CORS

Let’s enhance the configuration of CORS (cross-origin resource sharing) in your application. Add the following code to the **Program.cs** file.

```
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(builder =>
    {
        builder.AllowAnyHeader()
               .AllowAnyMethod()
               .SetIsOriginAllowed((host) => true);
    });
});
```

CORS is a security mechanism that prevents web pages or scripts from requesting a domain other than the one that served the original web page. In this instance, the **AddCors** method adds a CORS policy to the app’s services. The ** AddDefaultPolicy** method is then invoked to add a default policy to the options object.

To activate CORS, include the **UseCors** method like in the following code example.

```
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors();
```

This code allows any header and method from any origin, providing flexibility while developing, but remember to restrict it to known origins when deploying for security reasons.

**Note:** The [UseCors](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.corsmiddlewareextensions.usecors)
 must be called after the **UseRouting** and ** UseStaticFiles** methods. Refer to the [Middleware order](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0#middleware-order)
 documentation to learn more about middleware order in ASP.NET Core.

Now, we’re done with the server configuration. Let’s move to the client side of the app.

## Install Apollo Angular

The **ClientApp** folder of our project contains the code for the UI app. Open a command window/terminal in this folder and execute the following command.

```
ng add apollo-angular
```

You’ll be prompted to input the URL for the GraphQL API. Type in the base URL of your app, appending **/graphql,** at the end. For instance, in this case, the URL for the GraphQL API would be https://localhost:44469/graphql/.

Following this, you’ll be asked to specify the version of GraphQL. You can input a custom version or press **Enter** to choose the default GraphQL version that Apollo Angular supports.

Once these steps are completed, the command will generate a file named **graphql.module.ts**. This file will contain all the configurations specific to GraphQL.

## Add the GraphQL query and mutation

Follow these steps to add the GraphQL query and mutation:

**Step 1****:** Create a folder named **GraphQL** under the ** src\app** directory.

**Step 2:** Create a file named **query.ts** within the ** GraphQL** folder. This file will contain the GraphQL query to fetch the genre list. Then, add the following code to the ** query.ts** file.

```
import { gql } from 'apollo-angular';
export const GET_GENRE = gql`
  query FetchGenreList {
    genreList {
      genreId
      genreName
    }
  }
`;
```

This query **FetchGenreList** retrieves the ** genreId** and ** genreName** from the ** genreList**.

**Step 3:** Similarly, create another file named **mutation.ts** in the ** GraphQL** folder. This file will contain the mutation that allows us to add a new movie record. Then, add the following code to the ** mutation.ts** file.

```
import { gql } from 'apollo-angular';
export const ADD_MOVIE = gql`
  mutation AddEmployeeData($movieData: MovieInput!) {
    addMovie(movie: $movieData) {
      movie {
        title
      }
    }
  }
`;
```

This mutation, **AddEmployeeData**, takes a ** MovieInput** object as a parameter and calls the ** addMovie** mutation on the server. It then retrieves the ** title** of the newly added movie.

## Create client-side models

Create a folder called **models** under the ** src\app** folder and add a file called ** genre.ts**. Then, add the following code inside it.

```
export interface Genre {
  genreId: number;
  genreName: string;
}
export type GenreType = {
  genreList: Genre[];
};
```

In the previous code example, we added an interface for **Genre**. We also created a type called ** GenreType,** which will be used as the type for the data returned from the GraphQL server.

Next, create a file called **movie.ts** and add the following code inside it.

```
export interface Movie {
  movieId: number;
  title: string;
  overview: string;
  genre: string;
  language: string;
  duration: number;
  rating: number;
  posterPath: string;
}
```

## Create the required services

Let’s create the necessary services to handle API calls to the GraphQL server:

**Step 1:** Run the following command in the ** ClientApp** folder to generate a service file.

```
ng g s services\fetch-genre
```

Then, add the following code to the **fetch-genre.service.ts** file.

```
import { Injectable } from '@angular/core';
import { Query } from 'apollo-angular';
import { GenreType } from '../models/genre';
import { GET_GENRE } from '../GraphQL/query';
@Injectable({
  providedIn: 'root',
})
export class FetchGenreService extends Query {
  document = GET_GENRE;
}
```

The **FetchGenreService** class extends the ** Query** class and requires a ** document** property to execute the GraphQL query. By setting the ** document** property to the ** GET_GENRE** query, this class can retrieve a list of movie genres from the server.

**Step 2:** Run the command ** ng g s services\movie-helper** to create a new service file. Then, add the following code to the ** movie-helper.service.ts** file.

```
import { Injectable } from '@angular/core';
import { FetchGenreService } from './fetch-genre.service';
import { map, shareReplay } from 'rxjs';
@Injectable({
  providedIn: 'root',
})
export class MovieHelperService {
  genreList$ = this.fetchGenreService.watch().valueChanges.pipe(
    map((result) => result?.data),
    shareReplay(1)
  );
  constructor(private readonly fetchGenreService: FetchGenreService) {}
}
```

The **MovieHelperService** retrieves a list of movie genres from the server using the ** FetchGenreService** class. The ** watch()** method returns an observable that emits the result of the GraphQL query. The ** valueChanges** property extracts the data from the result. The ** shareReplay()** operator caches the query result so that subsequent subscribers to the ** genreList$** observable receive the cached result instead of making a new request to the server. This is beneficial, as the list of genres doesn’t change frequently, so we cache the result on the client instead of making a server call each time.

**Step 3:** Create a new service called **add-movie.service.ts** and add the following code to it.

```
import { Injectable } from '@angular/core';
import { Mutation } from 'apollo-angular';
import { ADD_MOVIE } from '../GraphQL/mutation';
@Injectable({
  providedIn: 'root',
})
export class AddMovieService extends Mutation {
  document = ADD_MOVIE;
}
```

The **AddMovieService** class extends the ** Mutation** class and requires a ** document** property to execute the GraphQL mutation. By setting the ** document** property to the ** ADD_MOVIE** mutation, this class can add a new movie to the server.

## Create the admin module

Our app incorporates role-based access, and only the Administrator has the permission to add, edit, and delete movie data. We’ll create a separate lazy-loaded module for admin functionalities to ensure the separation of concerns and improve the app’s startup performance.

**Step 1:** Run the following command to generate the Admin module.

```
ng g m admin
```

**Step 2:** Execute the following command to create the Admin routing module.

```
ng g m admin\admin-routing
```

**Step 3:** We’ll use [Syncfusion Angular controls](https://ej2.syncfusion.com/angular/documentation/introduction)
 in our app, so we need a module to handle the necessary imports. Run the following command to create this module.

```
ng g m admin\admin-ej2-components
```

This module will manage the imports for the Syncfusion Angular controls used in the lazy-loaded Admin module.

**Step 4:** Next, run the following command to generate the movie form component.

```
ng g c admin\components\movie-form
```

We’ll add the business logic to this component later in this article.

**Step 5:** Update the ** AdminRoutingModule** by referring to the following code example.

```
import { NgModule } from '@angular/core';
import { MovieFormComponent } from '../components/movie-form/movie-form.component';
import { RouterModule, Routes } from '@angular/router';
const adminRoutes: Routes = [
  {
    path: '',
    children: [
      { path: 'new', component: MovieFormComponent },
      { path: ':movieId', component: MovieFormComponent },
    ],
  },
];
@NgModule({
  imports: [RouterModule.forChild(adminRoutes)],
  exports: [RouterModule],
})
export class AdminRoutingModule {}
```

The **AdminRoutingModule** defines the routes for the admin section of the app. The ** adminRoutes** constant is an array of ** Routes** objects that define the child routes for the admin section.

**Step 6:** Update the ** AdminModule** by referring to the following code example.

```
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MovieFormComponent } from './components/movie-form/movie-form.component';
import { AdminRoutingModule } from './admin-routing/admin-routing.module';
import { ReactiveFormsModule } from '@angular/forms';
import { AdminEj2ComponentsModule } from './admin-ej2-components/admin-ej2-components.module';
@NgModule({
  declarations: [MovieFormComponent],
  imports: [
    CommonModule,
    AdminRoutingModule,
    ReactiveFormsModule,
    AdminEj2ComponentsModule,
  ],
})
export class AdminModule {}
```

Thus, we’ve added the imports for **AdminRoutingModule**, ** ReactiveFormsModule**, and ** AdminEj2ComponentsModule**.

**Step 7:** Once you have installed the packages, add the required imports in the ** admin-ej2-components.module.ts** file, as shown in the following code.

```
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UploaderModule } from '@syncfusion/ej2-angular-inputs';
import { ButtonModule } from '@syncfusion/ej2-angular-buttons';
import { TextBoxModule } from '@syncfusion/ej2-angular-inputs';
import { DropDownButtonModule } from '@syncfusion/ej2-angular-splitbuttons';
import { ToastModule } from '@syncfusion/ej2-angular-notifications';
import {
  GridModule,
  SortService,
  PageService,
  SearchService,
  ToolbarService,
} from '@syncfusion/ej2-angular-grids';
import { TooltipModule } from '@syncfusion/ej2-angular-popups';
import { DropDownListModule } from '@syncfusion/ej2-angular-dropdowns';
const importedModules = [
  ButtonModule,
  TextBoxModule,
  UploaderModule,
  DropDownButtonModule,
  ToastModule,
  GridModule,
  TooltipModule,
  DropDownListModule,
];
@NgModule({
  imports: [CommonModule, importedModules],
  exports: importedModules,
  providers: [PageService, SortService, SearchService, ToolbarService],
})
export class AdminEj2ComponentsModule {}
```

**Note:** Ensure that the necessary CSS reference for Syncfusion Angular components is included in the **src\styles.css** file. For additional details on how to install and configure Syncfusion Angular controls, kindly refer to the [Angular getting started documentation](https://ej2.syncfusion.com/angular/documentation/introduction)
.

## Configure lazy loading for the Admin module

Now, let’s configure lazy loading for the Admin module by following these steps:

**Step 1:** First, we need to create an**app-routing module** by running the following command.

```
ng g m app-routing --flat
```

After executing this, the **–flat** option will create the module file at the top level of the current project root.

**Note:** Refer to the [Angular CLI documentation](https://angular.io/cli/generate#module-command)
 for more details.

**Step 2:** We will use the app-routing module to configure routes for our app. Add the following code to the ** app-routing.module.ts** file.

```
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
const appRoutes: Routes = [
  { path: '', component: HomeComponent, pathMatch: 'full' },
  {
    path: 'admin/movies',
    loadChildren: () =>
      import('./admin/admin.module').then((module) => module.AdminModule),
  },
];
@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes, {
      scrollPositionRestoration: 'top',
    }),
  ],
  exports: [RouterModule],
})
export class AppRoutingModule {}
```

In the code, a constant **appRoutes** is defined as an array of route objects. Each object in this array represents a route in the app. The first route is the root route (‘ ‘), which maps to the ** HomeComponent**. The ** pathMatch:’full’** property represents the entire URL path and needs to be matched; in this case, it means that ** HomeComponent** will be displayed when the app’s URL is its base URL.

The second route is for **admin/movies**. This route uses lazy loading, which means the ** AdminModule** (and all of its associated code) won’t be loaded until the user navigates to ** admin/movies**. This can help improve performance by only loading code as it’s needed.

The **RouterModule** is imported with the ** forRoot** method, which is supplied with the ** appRoutes** array and a configuration object. The ** scrollPositionRestoration: ‘top’** option means that the app will automatically scroll to the top of the page when navigating to a new route.

**Step 3:** Now, import the **AppRoutingModule** into the ** AppModule.** This allows the routes to be used in the app. Update the ** app.module.ts** file as follows.

```
@NgModule({
  // Existing code
  imports: [
    // Existing imports
    AdminRoutingModule,
  ],
})
```

**Step 4:** Then**,** create a module for Syncfusion Angular controls by running the command **ng g m ej2-components**. This module will handle the imports for the Syncfusion Angular controls that will be used in the eager-loaded app module. Add the following code to the ** ej2-components.module.ts** file.

```
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ButtonModule } from '@syncfusion/ej2-angular-buttons';
import { TextBoxModule } from '@syncfusion/ej2-angular-inputs';
import { DropDownListModule } from '@syncfusion/ej2-angular-dropdowns';
import { DropDownButtonModule } from '@syncfusion/ej2-angular-splitbuttons';
import { ToastModule } from '@syncfusion/ej2-angular-notifications';
import { TooltipModule } from '@syncfusion/ej2-angular-popups';
import { AutoCompleteModule } from '@syncfusion/ej2-angular-dropdowns';
import { MessageModule } from '@syncfusion/ej2-angular-notifications';
import { GridModule } from '@syncfusion/ej2-angular-grids';
import { AppBarModule } from '@syncfusion/ej2-angular-navigations';
const importedModules = [
  AppBarModule,
  ButtonModule,
  TextBoxModule,
  DropDownListModule,
  DropDownButtonModule,
  ToastModule,
  TooltipModule,
  AutoCompleteModule,
  MessageModule,
  GridModule,
];
@NgModule({
  imports: [CommonModule, importedModules],
  exports: importedModules,
})
export class Ej2ComponentsModule {}
```

**Step 5:** Make sure to add the required [CSS reference for Syncfusion Angular components](https://ej2.syncfusion.com/angular/documentation/appearance/material3#syncfusion-material-3-theme)
 in the **src\styles.css** file.

## Configure the movie form component

In this section, we’ll configure the **movie-form** component, which will be used for both adding and editing movie data. We’ll utilize Angular’s reactive forms approach to create a form that allows users to add or edit movie data.

**Step 1:** First, create a new folder named ** models**under the** src\app\admin** folder.

**Step 2:** In the ** models** folder, add a new file named ** movie-form.ts**. This file will contain the interface for the movie form. Then, add the following code to it.

```
import { FormControl } from '@angular/forms';
export interface MovieForm {
  movieId: FormControl<number>;
  title: FormControl<string>;
  overview: FormControl<string>;
  genre: FormControl<string>;
  language: FormControl<string>;
  duration: FormControl<number>;
  rating: FormControl<number>;
}
```

This code defines an interface **MovieForm** with various form controls like ** movieId**, ** title**, ** overview**, ** genre**, ** language**, ** duration**, and ** rating**.

**Step 3:** Next, navigate to the ** src\app\admin\components\movie-form\movie-form.component.ts** file and add the following necessary code to the ** MovieFormComponent** class.

```
protected movieForm!: FormGroup;
protected formTitle = 'Add';
private destroyed$ = new ReplaySubject(1);
posterPreview!: ArrayBuffer | string | null;
posterFile = '';
moviePosterPath = '';
public fields: Object = { text: 'genreName', value: 'genreName' };
protected submitted = false;
genreList$ = this.movieHelperService.genreList$;
constructor(
    private readonly movieHelperService: MovieHelperService,
    private readonly formBuilder: NonNullableFormBuilder,
    private readonly addMovieService: AddMovieService,
    private readonly router: Router
  ) {
    this.initializeForm();
  }
uploadPoster(args: any) {
  const reader = new FileReader();
  reader.readAsDataURL(args.filesData[0].rawFile);
  reader.onloadend = (myevent) => {
    if (myevent.target?.result != null) {
        this.posterPreview = myevent.target.result;
        this.posterFile = (this.posterPreview as string).split(',')[1];
     }
  };
}
private initializeForm(): void {
   this.movieForm = this.formBuilder.group({
      movieId: 0,
      title: this.formBuilder.control('', Validators.required),
      genre: this.formBuilder.control('', Validators.required),
      language: this.formBuilder.control('', Validators.required),
      overview: this.formBuilder.control('', [
        Validators.required,
        Validators.maxLength(1000),
      ]),
      duration: this.formBuilder.control(0, [
        Validators.required,
        Validators.min(1),
      ]),
      rating: this.formBuilder.control(0, [
        Validators.required,
        Validators.min(0.0),
        Validators.max(10.0),
      ]),
   });
}
```

The **uploadPoster()** function is designed to manage the event triggered when a user uploads a movie poster. It constructs a new ** FileReader** object and reads the data from the uploaded file. The ** onloadend** event is subsequently utilized to assign the ** posterPreview** property to the outcome of the file read operation, which is a base64-encoded string of the file data. The ** posterFile** property is then assigned to the base64-encoded string of the file data, facilitating the transmission of the file data to the server.

The **initializeForm()** function is employed to set up the movie form. The **movieForm** property is assigned to a new ** FormGroup** object that encompasses the form controls for the movie title, genre, language, overview, duration, and rating. The ** Validators** class is used to establish the validation rules for each form control.

Please incorporate the following functions into the **MovieFormComponent** class.

```
protected get movieFormControl() {
    return this.movieForm.controls;
}
protected onFormSubmit(): void {
    this.submitted = true;
    if (!this.movieForm.valid) {
      return;
    }
    this.addMovie();
}
private addMovie() {
    const movieData: Movie = {
      movieId: this.movieForm.controls.movieId.value,
      title: this.movieForm.controls.title.value,
      duration: Number(this.movieForm.controls.duration.value),
      rating: Number(this.movieForm.controls.rating.value),
      genre: this.movieForm.controls.genre.value,
      language: this.movieForm.controls.language.value,
      overview: this.movieForm.controls.overview.value,
      posterPath: this.posterFile,
    };
    this.addMovieService
      .mutate({ movieData: movieData })
      .pipe(takeUntil(this.destroyed$))
      .subscribe({
        next: () => {
          ToastUtility.show({
            content: 'The movie data is added successfully.',
            position: { X: 'Right', Y: 'Top' },
            cssClass: 'e-toast-success',
          });
          this.router.navigate(['/']);
        },
        error: (error) => {
          ToastUtility.show({
            content: 'Error occurred while adding movie data.',
            position: { X: 'Right', Y: 'Top' },
            cssClass: 'e-toast-danger',
          });
          console.error('Error ocurred while adding movie data : ', error);
        },
      });
}
ngOnDestroy(): void {
    this.destroyed$.next();
    this.destroyed$.complete();
}
```

A **movieData** object is created with properties that correspond to the controls in a form. The value of each control is retrieved and assigned to the corresponding property in ** movieData**.

**Note:** The duration and rating values are converted to numbers using the **Number()** function.

The **mutate()** method is used to execute a GraphQL mutation that adds the new movie to the server. The ** pipe(takeUntil(this.destroyed$))** part is a common pattern in RxJS. It ensures that the subscription to the Observable returned by ** mutate()** is automatically unsubscribed from when the ** destroyed$** Observable emits a value. This helps prevent memory leaks.

The **subscribe()** method handles the response from the server. If the server responds successfully, the app navigates to the root route. If an error occurs while adding the movie data, a toast notification is displayed with an appropriate message, and the error is also logged into the console.

Open the **movie-form.component.html** file and add the following code to it.

```
<div class="row justify-content-center">
  <div
    class="title-container p-2 d-flex align-items-center justify-content-between"
  >
    <h2 class="m-0">{{ formTitle }} Movie</h2>
  </div>
  <div class="e-card">
    <div class="row justify-content-start">
      <div class="col-md-8">
        <div class="e-card-content row g-0">
          <form [formGroup]="movieForm" (ngSubmit)="onFormSubmit()">
            <div class="e-input-section">
              <ejs-textbox
                placeholder="Title"
                cssClass="e-outline"
                floatLabelType="Auto"
                formControlName="title"
              ></ejs-textbox>
            </div>
            <div
              *ngIf="(movieFormControl.title.touched || submitted)&&
                  movieFormControl.title.errors?.['required']"
              class="e-error"
            >
              Title is required.
            </div>
            <div class="e-input-section">
              <ejs-dropdownlist
                placeholder="Genre"
                cssClass="e-outline"
                floatLabelType="Auto"
                formControlName="genre"
                [dataSource]="(genreList$ | async)?.genreList"
                [fields]="fields"
              >
              </ejs-dropdownlist>
            </div>
            <div
              *ngIf="(movieFormControl.genre.touched || submitted) &&
                    movieFormControl.genre.errors?.['required']"
              class="e-error"
            >
              Genre is required.
            </div>
            <div class="e-input-section">
              <ejs-textbox
                placeholder="Language"
                cssClass="e-outline"
                floatLabelType="Auto"
                formControlName="language"
              ></ejs-textbox>
            </div>
            <div
              *ngIf="(movieFormControl.language.touched || submitted)&&
                    movieFormControl.language.errors?.['required']"
              class="e-error"
            >
              Language is required.
            </div>
            <div class="e-input-section">
              <ejs-textbox
                placeholder="Duration (mins)"
                cssClass="e-outline"
                floatLabelType="Auto"
                formControlName="duration"
              ></ejs-textbox>
            </div>
            <div
              *ngIf="(movieFormControl.duration.touched || submitted) &&
                      movieFormControl.duration.errors?.['required']"
              class="e-error"
            >
              Duration is required.
            </div>
            <div
              *ngIf="movieFormControl.duration.touched &&
                    movieFormControl.duration.errors?.['min']"
              class="e-error"
            >
              Duration cannot be less than 0.
            </div>
            <div class="e-input-section">
              <ejs-textbox
                placeholder="Rating"
                cssClass="e-outline"
                floatLabelType="Auto"
                formControlName="rating"
              ></ejs-textbox>
            </div>
            <div
              *ngIf="(movieFormControl.rating.touched || submitted) &&
                        movieFormControl.rating.errors?.['required']"
              class="e-error"
            >
              Rating is required.
            </div>
            <div
              *ngIf="
                (movieFormControl.rating.touched || submitted) &&
                (movieFormControl.rating.hasError('min') ||
                  movieFormControl.rating.hasError('max'))
              "
              class="e-error"
            >
              The value should be between 0 and 10.
            </div>
            <div class="e-input-section">
              <ejs-textbox
                placeholder="Overview"
                cssClass="e-outline"
                floatLabelType="Auto"
                formControlName="overview"
                [multiline]="true"
              ></ejs-textbox>
            </div>
            <div
              *ngIf="(movieFormControl.overview.touched || submitted) &&
                        movieFormControl.overview.errors?.['required']"
              class="e-error"
            >
              Overview is required.
            </div>
            <div
              *ngIf="movieFormControl.overview.touched &&
                        movieFormControl.overview.errors?.['maxlength']"
              class="e-error"
            >
              Overview cannot be more than 1000 characters long.
            </div>
            <div class="e-card-actions d-flex justify-content-end">
              <button type="submit" ejs-button cssClass="e-info">Save</button>
              <button ejs-button cssClass="e-danger" class="ms-2">
                Cancel
              </button>
            </div>
          </form>
        </div>
      </div>
      <div class="col-md-4 d-flex flex-column align-items-center">
        <div class="e-card-content row g-0 justify-content-center">
          <img
            class="preview-image p-2"
            src="{{ posterPreview }}"
            alt="Upload an image"
          />
          <div class="my-2">
            <ejs-uploader
              #defaultupload
              id="fileupload"
              (selected)="uploadPoster($event)"
            ></ejs-uploader>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
```

We have created a form that uses Syncfusion’s Angular UI components for the form fields and buttons. The form’s title is set based on the **formTitle** property of the component. This allows the form to be used for both adding and editing movies.

The **movieForm** is a ** FormGroup** instance that tracks the value and validity state of the form controls. The ** formControlName** attribute is used to link each form control with a ** FormControl** instance in the ** movieForm** ** FormGroup**. Each form field has corresponding validation messages.

The ejs-uploader component is used to upload a poster image for the movie. When a file is selected, the **uploadPoster()** method is called. The **{{ posterPreview }}** expression is used to display a preview of the uploaded poster image.

The Save button submits the form, triggering the **onFormSubmit()** method. The event for the Cancel button will be added in the next part of this series of articles.

**Note:** To learn more about the Reactive form, refer to this [Mastering Strictly Typed Reactive Forms in Angular](https://www.syncfusion.com/blogs/post/strictly-typed-reactive-forms-in-angular.aspx)
 blog.

## Configure the navigation bar

Run the following command to create the **nav-bar** component.

```
ng g c components\nav-bar --m app
```

The **–m** flag allows us to specify the module where we want to declare the component. To learn more, refer to the [Angular component documentation](https://angular.io/cli/generate#component-command)
.

Then, open the **nav-bar.component.html** file and add the following code to it.

```
<ejs-appbar colorMode="Primary">
  <button
    ejs-button
    iconCss="e-icons e-video e-medium"
    cssClass="e-inherit"
    [routerLink]="['/']"
  >
    Movie App
  </button>
  <div class="e-appbar-spacer"></div>
  <button
    ejs-button
    cssClass="e-inherit"
    [routerLink]="['/admin/movies/new']"
    [isToggle]="true"
  >
    Add movie
  </button>
</ejs-appbar>
```

## How do we get the data for the demo?

You can get dummy data to test the app from any source you like. However, for consistency, we will use [The Movie Database](https://www.themoviedb.org/)
 as the data source. You can get all the required details for the app, such as movie title, rating, genre, and posters, from this website.

## Execution demo

Finally, launch the app. You will see an **Add Movie** option on the nav bar. Click on it and add the required movies, as shown in the following GIF image.![Full stack web movie app using Angular & GraphQL](https://www.syncfusion.com/blogs/wp-content/uploads/2024/01/Final-Display-of-an-Angular-GraphQL-Movie-App.gif)

After a movie has been added successfully, you can verify the data inside the **Movie** table in the database.

Once the app is executed for the first time, the Poster folder will be created in the \wwwroot folder of the project. Add a default image of your choice in this folder with the name **DefaultPoster.jpg**. This image will act as the default poster image if the user does not upload an image. Remember, we already configured the logic to use the default poster earlier in this article.

## GitHub reference

Check out the complete source code of this [full-stack web app with Angular and GraphQL](https://github.com/SyncfusionExamples/full-stack-web-app-using-angular-and-graphql)
 on the GitHub repository.


## Summary

We appreciate your time in reading this article. We embarked on a journey to understand the creation of mutations in a GraphQL server, utilizing the Apollo GraphQL client to interact with server endpoints. This allowed us to implement a feature to add new movie data to our application.

In the next article of this series, we will enhance our app by introducing edit and delete functionalities for movie data. Additionally, we will configure the home page to showcase a list of movies, complete with sorting and filtering options, for a user-friendly experience.

If you’re not a Syncfusion user, we encourage you to explore our [Angular components](https://www.syncfusion.com/angular-components)
 through our [free trial](https://www.syncfusion.com/downloads/angular)
.

If you require any assistance or have any questions, feel free to reach out to us via our [support forum](https://www.syncfusion.com/forums)
, [support portal](https://support.syncfusion.com/)
, or [feedback portal](https://www.syncfusion.com/feedback/angular)
. We are always happy to help you!

## Related Blogs



[A Full-Stack Web App Using Angular and GraphQL: Part 1](https://www.syncfusion.com/blogs/post/full-stack-web-app-angular-graphql)



[A Full-Stack Web App Using Angular and GraphQL: Perform Edit, Delete, and Advanced Filtering (Part 3)](https://www.syncfusion.com/blogs/post/full-stack-app-angular-graphql-3)



[A Full-Stack Web App Using Angular and GraphQL: Adding User Registration Functionality (Part 4)](https://www.syncfusion.com/blogs/post/full-stack-app-angular-graphql-4)



[A Full-Stack Web App Using Angular and GraphQL: Adding Login and Authorization Functionalities (Part 5)](https://www.syncfusion.com/blogs/post/full-stack-app-angular-graphql-5)



[Build a Dynamic Watchlist for Your Web App with Angular & GraphQL (Part 6)](https://www.syncfusion.com/blogs/post/build-dynamic-watchlist-angular-graphql)



[Deploy Angular App with GraphQL on IIS & Azure (Part 7)](https://www.syncfusion.com/blogs/post/deploy-graphql-angular-on-iss-azure-7)
