Develop a GraphQL-Powered API With Symfony

July 26, 2022
Written by
Oluyemi Olususi
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Develop a GraphQL-Powered API With Symfony

For years, REST (Representational State Transfer) has been the de facto standard when designing APIs. That's quite understandable, as it's a very straight-forward structure: After sending a request to an endpoint, a JSON (or XML) response is returned to the client.

However, as applications became more complicated a recurrent theme started to emerge - multiple REST calls are required to populate a view. Enter GraphQL. With GraphQL, the sender of the request determines the structure of the response, thus providing more flexibility and efficiency to the frontend.

In this article, I will show you how to develop a GraphQL-powered API for an online book store with Symfony.

Prerequisites

To get the most out of this tutorial, you need the following:

Getting Started

Create a new Symfony application, and switch into the newly generated directory, by running the following commands.

symfony new graphql_demo --version=5.4
cd graphql_demo

To provide integration with GraphQL, the application will use a Symfony bundle named overblog/graphql-bundle. At the time of writing, the overblog/graphql-bundle could not be installed to a Symfony 6 project hence the use of Symfony 5.4

Next, install the project dependencies. The project will use the following:

  1. Doctrine: The Doctrine ORM help manages the application database.
  2. DoctrineFixturesBundle: This helps load authors and books into the database.
  3. Faker: Generates fake data for the application.
  4. Maker: Simplifies creating controllers, entities, and the like.
  5. OverblogGraphQLBundle: This bundle provides integration with GraphQL.
  6. OverblogGraphiQLBundle: This bundle provides an integration of the GraphiQL interface.

To install them, run the two commands below.

composer require overblog/graphql-bundle orm -W
composer require --dev overblog/graphiql-bundle maker orm-fixtures fakerphp/faker

Accept the contrib recipes installation from Symfony Flex by pressing a. Then, create a .env.local file from the .env file, which Symfony generated during the creation of the project, by running the command below.

cp .env .env.local

This file is ignored by Git as it matches an existing pattern in .gitignore (which Symfony generated). One of the reasons for this file is the accepted best practice of storing your credentials outside of code to keep them safe.

Next, update the DATABASE_URL parameter in .env.local so that the app uses an SQLite database instead of the PostgreSQL default. To do that, comment out the existing DATABASE_URL entry and uncomment the SQLite option, so that it matches the example below.

DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"

The database will be created in ./var and be named data.db.

Create entities

The application will have two entities, Author and Book. Create the Author entity first using the following command.

symfony console make:entity Author

The CLI will ask some questions and add fields for the entity based on the provided answers. Answer the questions as shown below.

New property name (press <return> to stop adding fields):
 > name

 Field type (enter ? to see all types) [string]:
 > string

 Field length [255]:
 > 255

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Author.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > dateOfBirth

 Field type (enter ? to see all types) [string]:
 > datetime

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Author.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > bio

 Field type (enter ? to see all types) [string]:
 > text

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Author.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 >

Press the "Enter" key to complete the creation process for the Author entity.

Next, create the Book entity using the following command.

symfony console make:entity Book

Respond to the CLI prompt as shown below.

New property name (press <return> to stop adding fields):
 > title

 Field type (enter ? to see all types) [string]:
 > string

 Field length [255]:
 > 255

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

Add another property? Enter the property name (or press <return> to stop adding fields):
 > author

 Field type (enter ? to see all types) [string]:
 > ManyToOne

 What class should this entity be related to?:
 > Author

 Is the Book.author property allowed to be null (nullable)? (yes/no) [yes]:
 > no

 Do you want to add a new property to Author so that you can access/update Book objects from it - e.g. $author->getBooks()? (yes/no) [yes]:
 > yes

 A new property will also be added to the Author class so that you can access the related Book objects from it.

 New field name inside Author [books]:
 > books

 Do you want to activate orphanRemoval on your relationship?
 A Book is "orphaned" when it is removed from its related Author.
 e.g. $author->removeBook($book)
 
 NOTE: If a Book may *change* from one Author to another, answer "no".

 Do you want to automatically delete orphaned App\\Entity\\Book objects (orphanRemoval)? (yes/no) [no]:
 > yes

 updated: src/Entity/Book.php
 updated: src/Entity/Author.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > synopsis

 Field type (enter ? to see all types) [string]:
 > text

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Book.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > releaseYear

 Field type (enter ? to see all types) [string]:
 > string

 Field length [255]:
 > 4

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Book.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > genre

Field type (enter ? to see all types) [string]:
 > string

 Field length [255]:
 > 255

Can this field be null in the database (nullable) (yes/no) [no]:
 > no

Add another property? Enter the property name (or press <return> to stop adding fields):
 > averageRating

 Field type (enter ? to see all types) [string]:
 > integer

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Book.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > copiesSold

 Field type (enter ? to see all types) [string]:
 > integer

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Book.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 >

Press the "Enter" key to complete the creation process for the Book entity.

Update the entities

Open the Author entity in the src/Entity/Author.php file and update its content to match the following.

<?php

namespace App\Entity;

use App\Repository\AuthorRepository;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: AuthorRepository::class)]
class Author 
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private ?int $id;

    #[ORM\Column(type: 'string', length: 255)]
    private string $name;

    #[ORM\Column(type: 'datetime')]
    private DateTimeInterface $dateOfBirth;

    #[ORM\Column(type: 'text')]
    private string $bio;

    #[ORM\OneToMany(
        mappedBy: 'author',
        targetEntity: Book::class,
        orphanRemoval: true
    )]
    private Collection $books;

    public function __construct(
        string            $name,
        DateTimeInterface $dateOfBirth,
        string            $bio
    ) {
        $this->name = $name;
        $this->dateOfBirth = $dateOfBirth;
        $this->bio = $bio;
        $this->books = new ArrayCollection();
    }

    public function getId(): ?int 
    {
        return $this->id;
    }

    public function getName(): ?string 
    {
        return $this->name;
    }

    public function setName(string $name): void 
   {
        $this->name = $name;
    }

    public function getDateOfBirth(): string 
    {
        return $this->dateOfBirth->format('l F jS, Y');
    }

    public function setDateOfBirth(DateTimeInterface $dateOfBirth): void 
    {
        $this->dateOfBirth = $dateOfBirth;
    }

    public function getBio(): ?string 
    {
        return $this->bio;
    }

    public function setBio(string $bio): void 
    {
        $this->bio = $bio;
    }

    public function getBooks(): Collection 
    {
        return $this->books;
    }

    public function addBook(Book $book): void 
    {
        if (!$this->books->contains($book)) {
            $this->books[] = $book;
            $book->setAuthor($this);
        }
    }

    public function removeBook(Book $book): void 
    {
        if ($this->books->removeElement($book)) {
            // set the owning side to null (unless already changed)
            if ($book->getAuthor() === $this) {
                $book->setAuthor(null);
            }
        }
    }
}

In addition to adding types for the fields of the Author entity, the revised code adds a constructor function that requires the author’s name, date of birth, and bio. It also modifies the getDateOfBirth() function to return a string corresponding to the date of birth, but in a human-readable format. Finally, it modifies the setter functions so that they return nothing, as they will not be used.

Lastly, open src/Entity/Book.php and update its content to match the following.

<?php

namespace App\Entity;

use App\Repository\BookRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: BookRepository::class)]
class Book 
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private ?int $id;

    #[ORM\Column(type: 'string', length: 255)]
    private string $title;

    #[ORM\ManyToOne(targetEntity: Author::class, inversedBy: 'books')]
    #[ORM\JoinColumn(nullable: false)]
    private Author $author;

    #[ORM\Column(type: 'text')]
    private string $synopsis;

    #[ORM\Column(type: 'string', length: 4)]
    private string $releaseYear;

    #[ORM\Column(type: 'string')]
    private string $genre;

    #[ORM\Column(type: 'integer')]
    private int $averageRating;

    #[ORM\Column(type: 'integer')]
    private int $copiesSold;

    public function __construct(
        string $title,
        Author $author,
        string $synopsis,
        string $releaseYear,
        int    $averageRating,
        int    $copiesSold,
        string $genre
    ) {
        $this->title = $title;
        $this->author = $author;
        $this->synopsis = $synopsis;
        $this->releaseYear = $releaseYear;
        $this->averageRating = $averageRating;
        $this->copiesSold = $copiesSold;
        $this->genre = $genre;
    }

    public function getId(): ?int 
    {
        return $this->id;
    }

    public function getTitle(): ?string 
    {
        return $this->title;
    }

    public function setTitle(string $title): void 
    {
        $this->title = $title;
    }

    public function getAuthor(): ?Author 
    {
        return $this->author;
    }

    public function setAuthor(?Author $author): void 
    {
        $this->author = $author;
    }

    public function getSynopsis(): ?string 
    {
        return $this->synopsis;
    }

    public function setSynopsis(string $synopsis): void 
    {
        $this->synopsis = $synopsis;
    }

    public function getReleaseYear(): ?string 
    {
        return $this->releaseYear;
    }

    public function setReleaseYear(string $releaseYear): void 
    {
        $this->releaseYear = $releaseYear;
    }

    public function getAverageRating(): ?int 
    {
        return $this->averageRating;
    }

    public function setAverageRating(int $averageRating): void 
    {
        $this->averageRating = $averageRating;
    }

    public function getCopiesSold(): ?int 
    {
        return $this->copiesSold;
    }

    public function setCopiesSold(int $copiesSold): void 
    {
        $this->copiesSold = $copiesSold;
    }

    public function getGenre(): string 
    {
        return $this->genre;
    }

    public function setGenre(string $genre): void 
    {
        $this->genre = $genre;
    }

    public function __set(string $property, mixed $value): void 
    {
        if (property_exists($this, $property)) {
            $this->$property = $value;
        }
    }
}

The modifications to the Book entity are similar to the ones made for the Author entity. Types for the fields were added, a constructor function was declared, and the setter functions were modified to have void as their return type. In addition, a magic setter for the Book entity was also added, to support updating multiple fields in one request.

Next, update the database schema by running the following command.

symfony console doctrine:schema:update --force

Create fixtures

To give us interesting data when we’re developing the API, let’s create some fixtures which will load fake data into the database. Start by creating a fixture to load authors by running the following command.

symfony console make:fixture AuthorFixtures

Then, open the newly created src/DataFixtures/AuthorFixtures.php file and update its content to match the following.

<?php

namespace App\DataFixtures;

use App\Entity\Author;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use Faker\Factory;
use Faker\Generator;

class AuthorFixtures extends Fixture 
{
    public const REFERENCE = 'AUTHORS_REFERENCE';

    private Generator $faker;

    public function __construct() 
    {
        $this->faker = Factory::create();
    }

    public function load(ObjectManager $manager): void 
    {
        for ($i = 0; $i < 10; $i++) {
            $manager->persist(
                $this->getFakeAuthor()
            );
        }

        $referenceAuthor = $this->getFakeAuthor();
        $this->addReference(self::REFERENCE, $referenceAuthor);

        $manager->persist($referenceAuthor);
        $manager->flush();
    }

    private function getFakeAuthor(): Author 
    {
        return new Author(
            $this->faker->name(),
            $this->faker->dateTime(),
            $this->faker->sentences(4, true)
        );
    }
}

The fixture adds 10 authors to the database using details generated by Faker. Before saving the changes to the database the addReference() function is called, passing to it the REFERENCE constant and an author. This provides access to the author in the fixture for loading books.

Create a new fixture for the Book entity using the following command.

symfony console make:fixture BookFixtures

Open the newly created src/DataFixtures/BookFixtures.php file and update its content to match the following.

<?php

namespace App\DataFixtures;

use App\Entity\Book;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;
use Faker\Factory;
use Faker\Generator;

class BookFixtures extends Fixture implements DependentFixtureInterface 
{
    private Generator $faker;

    public function __construct() 
    {
        $this->faker = Factory::create();
    }

    public function load(ObjectManager $manager): void 
    {
        for ($i = 0; $i < 100; $i++) {
            $manager->persist(
                $this->getFakeBook()
            );
        }
        $manager->flush();
    }

    private function getFakeBook(): Book 
    {
        $genres = ['Action', 'Comedy', 'Romance', 'Sci-fi', 'Programming'];

        return new Book(
            $this->faker->sentence(),
            $this->getReference(AuthorFixtures::REFERENCE),
            $this->faker->sentences(5, true),
            $this->faker->year(),
            $this->faker->numberBetween(1, 10),
            $this->faker->numberBetween(10000, 10000000),
            $this->faker->randomElement($genres)
        );
    }

    public function getDependencies(): array 
    {
        return [
            AuthorFixtures::class,
        ];
    }
}

The first thing to note is that this fixture implements what is called a DependentFixtureInterface. Because a book requires an author in its constructor, the AuthorsFixture must run before the BooksFixture. This dependency is specified in the getDependencies() function. By doing so, the fixtures run in the expected order.

The books are created using details from Faker. But notice that for the author parameter, the getReference() function is called. This retrieves the reference author in the AuthorFixture.

Run the fixtures using the following command.

symfony console doctrine:fixtures:load -n

Once the command finishes running, confirm that the database contains authors by running the following command.

symfony console doctrine:query:sql "select * from author;"

Implement GraphQL functionality

Define the schema

The next thing we will do is define the schema for our API. In the config/graphql/types folder, you’ll create a schema for the author and book entities. In addition, you’ll declare schemas for the mutations and queries.

The API will handle the following queries:

  1. Query for an author based on id
  2. Query for all authors
  3. Query for an author via the author’s name
  4. Query for all books
  5. Query for books based on genre
  6. Query for books based on id

For mutations, it will have the following mutations:

  1. Create a new author
  2. Update the details of a book

So, start with the Author entity. In the config/graphql/types folder, create a new file called Author.graphql and add the following to it.

type Author {
    id: ID!
    name: String!
    dateOfBirth: String!
    bio: String!
    books: [Book!]!
}

input CreateAuthor {
    name: String!
    dateOfBirth: String!
    bio: String!
}

Next, create another file in the config/graphql/types folder named Book.graphql and add the following to it.

type Book {
    id: ID!
    title: String!
    author: Author!
    synopsis: String!
    releaseYear: String!
    genre: String!
    averageRating: Int!
    copiesSold: Int!
}

input UpdateBook {
    title: String
    author: ID
    synopsis: String
    releaseYear: String
    genre: String
    averageRating: Int
    copiesSold: Int
}

With the types and inputs for the Author and Book entities in place, define the schemas for the queries and mutations. To do this, in the config/graphql/types folder, create a new file called query.graphql and add the following code to it.

type RootQuery {
    author(id: ID!): Author
    authors: [Author!]!
    findBooksByAuthor(name: String!) : [Book!]!
    books: [Book!]!
    findBooksByGenre(genre: String!) : [Book!]!
    book(id:ID!): Book
}

This defines the permitted queries for the API, specifying the parameters (where required) and expected types. It also declares the return types for each query.

Next, create another file in the config/graphql/types folder named mutations.graphql and add the following to it.

type RootMutation {
    createAuthor(author: CreateAuthor!): Author!
    updateBook(id: ID!, book: UpdateBook!): Book!
}

Similar to the query schema, it specifies the required parameters for each mutation. It also use the inputs declared for creating an author as well as updating a book in the Author.graphql and Book.graphql schemas respectively.

Create the services

To separate concerns, next create services to handle the query and mutation requests. In the src folder at the root of the project, create a new folder called Service.

Create a query service

In the src/Service folder, create a new file called QueryService.php and add the following to it.

<?php

namespace App\Service;

use App\Entity\Author;
use App\Entity\Book;
use App\Repository\AuthorRepository;
use App\Repository\BookRepository;
use Doctrine\Common\Collections\Collection;

class QueryService 
{
    public function __construct(
        private AuthorRepository $authorRepository,
        private BookRepository   $bookRepository,
    ) {}

    public function findAuthor(int $authorId): ?Author 
    {
        return $this->authorRepository->find($authorId);
    }

    public function getAllAuthors(): array 
    {
        return $this->authorRepository->findAll();
    }

    public function findBooksByAuthor(string $authorName): Collection 
    {
        return $this
            ->authorRepository
            ->findOneBy(['name' => $authorName])
            ->getBooks();
    }

    public function findAllBooks(): array 
    {
        return $this->bookRepository->findAll();
    }

    public function findBooksByGenre(string $genre): array 
    {
        return $this->bookRepository->findBy(['genre' => $genre]);
    }

    public function findBookById(int $bookId): ?Book
    {
        return $this->bookRepository->find($bookId);
    }
}

In this service, we declare a function to handle all the requests specified in config/graphql/types/query.graphql. Using the AuthorRepository and BookRepository which were injected as constructor dependencies, we are able to retrieve books and authors based on the parameters provided in the request.

Create a mutation service

In the src/Service folder, create a new file called MutationService.php and add the following to it.

<?php

namespace App\Service;

use App\Entity\Author;
use App\Entity\Book;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use GraphQL\Error\Error;

class MutationService 
{
    public function __construct(
        private EntityManagerInterface $manager
    ) {}

    public function createAuthor(array $authorDetails): Author 
    {
        $author = new Author(
            $authorDetails['name'],
            DateTime::createFromFormat('d/m/Y', $authorDetails['dateOfBirth']),
            $authorDetails['bio']
        );

        $this->manager->persist($author);
        $this->manager->flush();

        return $author;
    }

    public function updateBook(int $bookId, array $newDetails): Book 
    {
        $book = $this->manager->getRepository(Book::class)->find($bookId);

        if (is_null($book)) {
            throw new Error("Could not find book for specified ID");
        }

        foreach ($newDetails as $property => $value) {
            $book->$property = $value;
        }

        $this->manager->persist($book);
        $this->manager->flush();

        return $book;
    }
}

The service receives an EntityManagerInterface for persisting and flushing changes to the database.

In the createAuthor() function, we create a new author from the information provided in the authorDetails array. One key thing to note is the date format which the API is expecting for the author’s date of birth (dd/mm/yyyy).

In the updateBook() function, we retrieve the book based on the provided id. If the book doesn’t exist, we throw an Error with an appropriate message. If we find a book with the specified id, we then update the properties based on the information provided in the update request. Finally, we save the changes and return the updated book.

The QueryService and MutationService will be used by the ResolverMap which will be created next.

Create a resolver

Our API now has a schema, but we still need a way to resolve the queries/mutations and provide the required response. We do this via a ResolverMap. In the src folder at the root of the project, create a new folder called Resolver. In it, create a new file called CustomResolverMap.php and add the following code to it.

<?php

namespace App\GraphQL\Resolver;

use App\Service\MutationService;
use App\Service\QueryService;
use ArrayObject;
use GraphQL\Type\Definition\ResolveInfo;
use Overblog\GraphQLBundle\Definition\ArgumentInterface;
use Overblog\GraphQLBundle\Resolver\ResolverMap;

class CustomResolverMap extends ResolverMap 
{
    public function __construct(
        private QueryService    $queryService,
        private MutationService $mutationService
    ) {}

    /**
     * @inheritDoc
     */
    protected function map(): array 
    {
        return [
            'RootQuery'    => [
                self::RESOLVE_FIELD => function (
                    $value,
                    ArgumentInterface $args,
                    ArrayObject $context,
                    ResolveInfo $info
                ) {
                    return match ($info->fieldName) {
                        'author' => $this->queryService->findAuthor((int)$args['id']),
                        'authors' => $this->queryService->getAllAuthors(),
                        'findBooksByAuthor' => $this->queryService->findBooksByAuthor($args['name']),
                        'books' => $this->queryService->findAllBooks(),
                        'findBooksByGenre' => $this->queryService->findBooksByGenre($args['genre']),
                        'book' => $this->queryService->findBookById((int)$args['id']),
                        default => null
                    };
                },
            ],
            'RootMutation' => [
                self::RESOLVE_FIELD => function (
                    $value,
                    ArgumentInterface $args,
                    ArrayObject $context,
                    ResolveInfo $info
                ) {
                    return match ($info->fieldName) {
                        'createAuthor' => $this->mutationService->createAuthor($args['author']),
                        'updateBook' => $this->mutationService->updateBook((int)$args['id'], $args['book']),
                        default => null
                    };
                },
            ],
        ];
    }
}

Here, we inject the QueryService and MutationService as constructor dependencies. Next, we override the map() function which expects an array as the return type. At the root of the array, we declare the resolvers for our RootQuery and RootMutation.

Using the information passed in the ResolveInfo object, we match the fieldName provided against the expected names which were declared in our query and mutation schemas. Based on the value of fieldName, we return the response from the associated service call, passing the provided arguments where required. As a default, we return null for unknown field names.

With our resolver in place, we can add our service configuration and run our API.

Configure the Overblog bundle

Open config/packages/graphql.yaml and update its content to match the following.

overblog_graphql:
    definitions:
        schema:
            mutation: RootMutation
            query: RootQuery
        mappings:
            types:
                -
                    type: graphql
                    dir: "%kernel.project_dir%/config/graphql/types"
                    suffix: null

Here, we change the mapping type from YAML to GraphQL which is the format used in declaring the API schema in config/graphql/types.

Next, we need to tag the resolver with the overblog_graphql.resolver_map tag. To do this, open config/services.yaml and update it to match the following.

parameters:

services:
    _defaults:
        autowire: true      
        autoconfigure: true 

    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'
            - '../src/Tests/'

    App\GraphQL\Resolver\CustomResolverMap:
        tags:
            - { name: overblog_graphql.resolver_map, schema: default }

With this in place, we can run our API using the following command.

symfony serve

By default, the application will run on port 8000. To access the GraphiQL interface and test your database, navigate to https://localhost:8000/graphiql.

Default Page

Test the endpoints

Return all books

Retrieve a list of books by running the following query in the query window:

query getAllBooks {
  books {
    id, 
    title,
    genre
  }
}

Collection of books

This returns the collection of books seeded in the database.

Find all authors

Run the following query to retrieve the list of authors

query authors {
  authors {
    id,
    name, 
    dateOfBirth,
    books {
      title,
      averageRating
    }
  }
}

This will return the id, name, dateOfBirth and books for the authors, as shown here:

List of Authors

Find an author with an id

Next, use the query below to fetch the details of a particular author using the author's id as a unique identifier:

query authors {
  author(id: 11) {
    name,
    dateOfBirth,
    bio,
    books {
      title
    }
  }
}

You will see the same query as shown in the image below:

Author using ID

Find books with a specific author

Next, to retrieve the list of books for a specific author using the author's name as the identifier, by using the following query:

{
  findBooksByAuthor(name:"Mr. Emmet Brekke") {
    title,
    genre
  }
}

Collection of book by an Author

Create an author

Here, we will start with mutations by using the following query to create a new author:

mutation CreateAuthor {
  createAuthor(author: {name: "Sample Author", dateOfBirth: "09/12/1990" , bio: "Great Author"}){
    name, 
    dateOfBirth
  }
}

Create Author

That's how to develop a GraphQL-Powered API with Symfony

In this article, you built a GraphQL powered API using Symfony. Using GraphQL, the client is able to specify the structure of the response (based on a previously agreed schema) thereby dealing with the issue of over or under-fetching. Additionally, we are able to leverage the type-checking provided out of the box to prevent unwanted bugs.

The code for this article is available here on GitHub. Happy coding!

Oluyemi is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest to solve day-to-day problems encountered by users, he ventured into programming and has since directed his problem-solving skills at building software for both web and mobile.

A full-stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and content on several blogs on the internet. Being tech-savvy, his hobbies include trying out new programming languages and frameworks.