May the Fourth Be With You

Michael Solati - May 4 '23 - - Dev Community

General Kenobi greeting a general of the Separatist Droid Army.

My first introduction to GraphQL was roughly 7 years ago, about a year after it was initially announced by Facebook. My first taste was using the "SWAPI," The Star Wars API, GraphQL wrapper that the team at Facebook created. You can view the source code for it here. It was an eye-opening experience, GraphQL felt like a game changer, but I only had an excuse to use it once joining Amplication.

So I thought today would be the best time to recreate that GraphQL API. Using tools like Amplication, ChatGPT, and SWAPI. A lot has changed since 2015, and we'll build this backend together in a breeze!

May the 4th Be With You...

What is Amplication

Before we start, you may wonder, "What even is Amplication?" Amplication is a developer tool to help generate backends in an instant. With a few clicks, you can create a Node.js server that supports GraphQL and REST APIs, that's secure using trusted libraries, that has authentication baked in, that automatically syncs with GitHub, and so much more!

We'll be using Amplication to build the GraphQL server for our backend.

When you realize you love using Amplication, remember to star our repository on GitHub.

Din Djarin saying 'This is the way.'

Building the Backend

To recreate the GraphQL API, it would be good to understand the original schema of our data. The GraphQL wrapper had information on people, starships, vehicles, species, planets that appeared in the films, and information about the films themselves. Our Amplication-generated backend will need to host all of this data and the relations between the data. So let's first get started stubbing out our project.

Visit app.amplication.com and sign in to Amplication with your GitHub account; if you haven't used Amplication before, that's ok... when you try to sign in, a new account will be created.

Amplication's welcome screen.


Amplication's welcome screen.

Creating a Project

Now let's create a new Project for our Star Wars and add a Service, which will be the GraphQL server serving all the Star Wars movies, characters, and more. The process of creating a Service has been recently improved by the release of our Service Wizard; you can learn more about it here.

Feel free to name your Service whatever you want, though I'll call mine Holocron.

Naming a new Service.


Naming a new Service.

The next step in the process is to configure the Service with a Git repository. As this is a new service, you may need to click "Go to project settings" to connect to GitHub, though if you have already, you should see a screen like the one below. Then, just click "Continue."

Connecting a Service to GithHub.


Connecting a Service to GithHub.

The main focus of this project is to build a GraphQL version of the Star Wars API, not a REST version. However, Amplication makes generating both REST and GraphQL API endpoints easy. So the work we do now will apply to both GraphQL and REST.

Choosing what type of APIs to generate.


Choosing what type of APIs to generate.

Amplication has made great strides in supporting mono and polyrepo projects for developers working with microservice architecture. However, this project will be in a unique state where we will not be developing a microservice project and will be syncing all of our project's code into one repository. As a result, selecting polyrepo will make sense for our usage as our one and only Service will be generated into the root of our repository.

Selecting how we want to sync our code to GitHub.


Selecting how we want to sync our code to GitHub.

When it comes to what database you want to use, you have options. You can use PostgreSQL, MongoDB, or MySQL for your backend. The choice is yours based on what you're comfortable with, but I'll use Postgres.

Selecting what database to use.


Selecting what database to use.

Select "Empty" so we have a blank workspace to work from when creating our Entities.

Selecting if you want to work from a template or not.


Selecting if you want to work from a template or not.

Our API will not require users to be authenticated to access data, so select "Skip Authentication."

Selecting if authentication should be baked into the app or not.


Selecting if authentication should be baked into the app or not.

Now we've set the base of our Service, and we can get into defining the data our backend will serve to users.

The Service Wizard completion screen.


The Service Wizard completion screen.

Adding our Entities

The Star Wars API serves 6 types of data: people, starships, vehicles, races, planets, and films. In Amplication, we refer to these data objects as Entities. One way to think of an Entity is like a table in SQL or a collection in NoSQL. So we'll be creating 6 entities or 6 tables.

So on the left-hand side, click the "Entities" button and then "Add entity +" to create your first Entity.

Person Entity

We'll call our first entity "Person" and add the following Fields. Each field should be Searchable with the Data Types shown in the sub-bullet points:

Field Data Type
Name Single Line Text
Height Whole Number
Mass Decimal Number
Hair Color Single Line Text
Skin Color Single Line Text
Eye Color Single Line Text
Birth Year Single Line Text
Gender Single Line Text
Description Multi Line Text

Creating the 'Person' Entity.


Creating the "Person"' Entity.

With the fields added to the Entity, we'll then want to set permissions for the Entity. Amplication, by default, requires all requests, mutations, and queries from an authenticated user. For this backend, we want to allow anyone to search it. Therefore, we want to set the View and Search actions to Public for the People entity and all other Entities we will create. Select Permissions on the left-hand side; you'll be greeted with a robust UI for protecting your entities. Set the View and Search actions to Public here, and disable all other options.

Configuring Permissions for an Entity.


Configuring Permissions for an Entity.

Starship Entity

Next, create a "Starship" entity:

Field Data Type
Name Single Line Text
Model Single Line Text
Manufacturer Json
Cost in Credits Decimal Number
Length Decimal Number
Max Atmosphering Speed Whole Number
Crew Single Line Text
Passengers Single Line Text
Cargo Capacity Decimal Number
Consumables Single Line text
Hyperdrive Rating Decimal Number
MGLT Whole Number
Starship Class Single Line Text
Description Multi Line Text

Vehicle Entity

For the "Vehicle" entity:

Field Data Type
Name Single Line Text
Model Single Line Text
Manufacturer Json
Cost in Credits Decimal Number
Length Decimal Number
Max Atmosphering Speed Whole Number
Crew Single Line Text
Passengers Single Line Text
Cargo Capacity Decimal Number
Consumables Single Line Text
Vehicle Class Single Line Text
Description Multi Line Text

Race Entity

For the "Race" entity:

Field Data Type
Name Single Line Text
Classification Single Line Text
Designation Single Line Text
Average Height Decimal Number
Average Lifespan Whole Number
Eye Colors Json
Skin Colors Json
Hair Colors Json
Language Single Line Text
Description Multi Line Text

Planet Entity

For the "Planet" entity:

Field Data Type
Name Single Line Text
Diameter Whole Number
Rotation Period Whole Number
Orbital Period Whole Number
Gravity Single Line Text
Population Decimal Number
Climates Json
Terrains Json
Surface Water Decimal Number
Description Multi Line Text

Film Entity

And finally, for the "Film" entity:

Field Data Type
Title Single Line Text
Episode ID Whole Number
Opening Crawl Single Line Text
Director Single Line Text
Producers Json
Release Date Date Time
Description Multi Line Text

Creating Relations

If it wasn't obvious, People could be associated with various Starships, they may appear in multiple Films, and have their own home Planet. These associations are referred to as Relations. We'll want to add the appropriate relations between our entities so users can query data about the entities they are curious about.

Person Entity Relations

Return to the Person entity and add a new field called "Planet." Amplication will understand that this field is likely a relation to the Planet entity and will automatically make the connection assuming that one
"Person" can be related to one "Planet."

Creating a relation between a Person and a Planet.
Creating a relation between a Person and a Planet.

Add the fields "Films," "Races," "Vehicles," and "Starships" to the Person entity, and Amplication will automagically build the correct relations between the entities.

Starship Entity Relations

Go into the Starship entity and click on the "Person" field. Then select "One 'Starship' can be related to many 'Person.'" Then add the field "Films."

Vehicle Entity Relations

In the Vehicle entity and click on the "Person" field. Then select "One 'Vehicle' can be related to many 'Person.'" Then add the field "Films."

Race Entity Relations

For the Race entity and click on the "Person" field. Then select "One 'Race' can be related to many 'Person.'" Then add the field "Films."

Planet Entity Relations

To the Planet entity, add the field "Films."

Film Entity Relations

Finally, for the Film entity, we'll revamp the relation fields. Update the "Person," "Starship," "Vehicle," "Race," and "Planet" fields so that one "Film" can be related to many of their respective entities.

With the Holocron service created, you'll want to run the code locally to continue this guide. This requires syncing code to GitHub and then cloning it locally. Assuming you're comfortable with git commands, such as git clone, check out Amplication docs site to learn how to sync your code to GitHub.

Completing the Archives

Impossible. Perhaps the archives are incomplete.

If you still need to clone the Amplication code from GitHub, now is the time. Once you've done that, open the server folder in your IDE (as that's where we'll be doing all of our work). Our custom code will depend on a few libraries not installed on the Amplication generated server. Run the following commands to install all existing dependencies and then add the remaining ones we need.

npm i
npm i -D isomorphic-unfetch crypto dotenv openai
Enter fullscreen mode Exit fullscreen mode

Then, inside the scripts folder, create a file called download.ts. This file will be a scraper of the Star Wars API so we have all the details of our characters, films, ships, and more. However, rather than creating it from scratch, we will modify the scraper script that Facebook created for their version of the GraphQL API. Most changes are just adding typing data as we write code in TypeScript rather than JavaScript. So rather than reviewing all of the changes, just copy and paste the code from here.

You'll want to run the download script, and you can do so with the following command ts-node scripts/download.ts scripts/data.json. Consider adding the command to your package.json scripts to make it repeatable.

Writing a Custom Seed

Running an Amplication project locally requires a few steps, including configuring and seeding your database. We fetched most of the data we'll want to populate into our backend. To get started, we need to generate the Prisma client, which the Node.js app uses to communicate with the database. Run the following command.

npm run prisma:generate
Enter fullscreen mode Exit fullscreen mode

First, in scripts/seed.ts, add the following code in the if statement :

  seed().catch((error) => {
    console.error(error);
    process.exit(1);
  });
Enter fullscreen mode Exit fullscreen mode

Now, open the file scripts/customSeed.ts and delete everything, then copy in the following.

import { PrismaClient } from "@prisma/client";
import crypto from "crypto";
import { Configuration, OpenAIApi } from "openai";
import allData from "./data.json";
Enter fullscreen mode Exit fullscreen mode

Getting Descriptions From OpenAI

The Star Wars API doesn't provide descriptions of characters, films, or anything. So while we could scrape the data from the web, we can also ask ChatGPT via the OpenAI APIs to generate descriptions. You'll need an OpenAI developer account, but create an API key and add it to the .env file as OPENAI_API_KEY.

Back in scripts/customSeed.ts add the following code:

const configuration = new Configuration({
  apiKey: process.env.OPENAI_API_KEY,
});
const openai = new OpenAIApi(configuration);

async function getDescription(name: string): Promise<any> {
  const completion = await openai.createCompletion({
    model: "text-davinci-003",
    max_tokens: 1000,
    prompt: `Give me a summary about "${name}" from Star Wars.`,
  });

  return completion.data.choices[0].text;
}
Enter fullscreen mode Exit fullscreen mode

The code above creates an OpenAI API client configured with our API key. Then we define a function, getDescription, which will take in the name of any film, character, etc., and ask OpenAI to generate a summary of said item.

Identifying Elements

We need access to real IDs of data from the Star Wars API to draw relations between our Entities. However, all elements are identified by their unique URL. So we can hash these URLs into something that looks more like an ID. That way, we can create relations between our data more efficiently. Add the following code as well:

function generateId(key: string): string {
  return crypto.createHash('md5').update(key).digest('hex');
}
Enter fullscreen mode Exit fullscreen mode

Seeding Data

Finally, we need to add our data. Again, this requires a bit of leg work, especially when it comes to cleaning and formatting the data. Replace the customSeed function with the following and I'll explain a bit of what's going on.

export async function customSeed() {
  const client = new PrismaClient();
// Get the URLs of all the data from the Star Wars API, but filter out summary pages
  const keys = Object.keys(allData).filter(k => /\d\/$/.test(k));

  // Seed Data
  for (const key of keys) {
    const data: { [key: string]: any } = (allData as any)[key];
    const id = generateId(key);
    const description = await getDescription(data.name || data.title);

    // Seed People
    if (key.includes('people')) {
      const person = {
        id,
        name: data.name,
        height: parseInt(data.height) || 0,
        mass: parseFloat(data.mass) || 0,
        hairColor: data.hair_color,
        skinColor: data.skin_color,
        eyeColor: data.eye_color,
        birthYear: data.birth_year,
        gender: data.gender,
        description
      };

      await client.person.upsert({
        where: { id },
        update: person,
        create: person,
      });
    }
  }

  client.$disconnect();
}
Enter fullscreen mode Exit fullscreen mode

The data.json file is all the data we ripped from the Star Wars API with the script from the Facebook team. It's stored as an Object, where every key is the URL of a query, and the object is the data. So first, we'll take all the keys and filter out the queries that provide a summary of the query. Then, we'll loop through the filtered keys/URLs and add the data to our backend.

As we iterate over all the keys, we'll create a data object that stores all the data belonging to a URL. We then generate an ID for that URL. We also generate our description from OpenAI. Note we're going to make a lot of requests, and you may get rate-limited... Consider adding logic to slow down the time between queries or something else. If you can't get it to work, you can always just set the description variable to an empty strong.

The fastest way to identify the type of Entity our key belongs to is by examining the URL/key. For example, for a Person, it will have "people" in the URL, and we can use that to identify the current data we're working with represents a person.

Once we know what type of object we're working with, we can create a clean data object, with all the fields converted or parsed from the data.json. Finally, we upsert the database with the cleansed object. Upserting is good when you want to create an entry in your database but are unsure if it already exists. This is helpful when seeding, as a seed may run multiple times. Also, sometimes updating data, but you want to avoid throwing an error if you try to create an element that already exists, or you want to write code to verify if an element exists or not, then try to create or update that element.

For the remaining Entities, add the following code to the for loop.

// Seed Starships
if (key.includes('starships')) {
  const starship = {
    id,
    name: data.name,
    model: data.model,
    manufacturer: data.manufacturer.split(', '),
    costInCredits: parseFloat(data.cost_in_credits) || 0,
    length: parseFloat(data.length) || 0,
    maxAtmospheringSpeed: parseInt(data.max_atmosphering_speed) || 0,
    crew: data.crew,
    passengers: data.passengers,
    cargoCapacity: parseFloat(data.cargo_capacity) || 0,
    consumables: data.consumables,
    hyperdriveRating: parseFloat(data.hyperdrive_rating) || 0,
    mglt: parseInt(data.MGLT) || 0,
    starshipClass: data.starship_class,
    description
  };

  await client.starship.upsert({
    where: { id },
    update: starship,
    create: starship,
  });
}

// Seed Vehicles
if (key.includes('vehicles')) {
  const vehicle = {
    id,
    name: data.name,
    model: data.model,
    manufacturer: data.manufacturer.split(', '),
    costInCredits: parseFloat(data.cost_in_credits) || 0,
    length: parseFloat(data.length) || 0,
    maxAtmospheringSpeed: parseInt(data.max_atmosphering_speed) || 0,
    crew: data.crew,
    passengers: data.passengers,
    cargoCapacity: parseFloat(data.cargo_capacity) || 0,
    consumables: data.consumables,
    vehicleClass: data.vehicle_class,
    description
  };

  await client.vehicle.upsert({
    where: { id },
    update: vehicle,
    create: vehicle,
  });
}

// Seed Races
if (key.includes('species')) {
  const race = {
    id,
    name: data.name,
    classification: data.classification,
    designation: data.designation,
    averageHeight: parseFloat(data.average_height) || 0,
    averageLifespan: parseInt(data.average_lifespan) || 0,
    skinColors: data.skin_colors.split(', '),
    hairColors: data.hair_colors.split(', '),
    eyeColors: data.eye_colors.split(', '),
    language: data.language,
    description
  };

  await client.race.upsert({
    where: { id },
    update: race,
    create: race,
  });
}

// Seed Planets
if (key.includes('planets')) {
  const planet = {
    id,
    name: data.name,
    diameter: parseInt(data.diameter) || 0,
    rotationPeriod: parseInt(data.rotation_period) || 0,
    orbitalPeriod: parseInt(data.orbital_period) || 0,
    gravity: data.gravity,
    population: parseFloat(data.population) || 0,
    surfaceWater: parseFloat(data.surface_water) || 0,
    climates: data.climate.split(', '),
    terrains: data.terrain.split(', '),
    description
  };

  await client.planet.upsert({
    where: { id },
    update: planet,
    create: planet,
  });
}

// Seed Films
if (key.includes('films')) {
  const film = {
    id,
    title: data.title,
    episodeId: data.episode_id,
    openingCrawl: data.opening_crawl,
    director: data.director,
    producers: data.producer.split(', '),
    releaseDate: new Date(data.release_date),
    description
  };

  await client.film.upsert({
    where: { id },
    update: film,
    create: film,
  });
}
Enter fullscreen mode Exit fullscreen mode

Now it comes time to add our relations. With all of our entities added at this point of the script, we can update each one to connect them to their respective planet, vehicles, film, and so on. Add the following code after the for loop but before the client.$disconnect().

// Add relations
for (const key of keys) {
  const data: { [key: string]: any } = (allData as any)[key];
  const id = generateId(key);

  // People's Relations
  if (key.includes('people')) {
    await client.person.update({
      where: { id },
      data: {
        planet: {
          connect: {
            id: generateId(data.homeworld)
          }
        },
        races: {
          connect: data.species.map((s: string) => ({
            id: generateId(s)
          }))
        },
        vehicles: {
          connect: data.vehicles.map((v: string) => ({
            id: generateId(v)
          }))
        },
        starships: {
          connect: data.starships.map((s: string) => ({
            id: generateId(s)
          }))
        }
      }
    });
  }

  // Films' Relations
  if (key.includes('films')) {
    await client.film.update({
      where: { id },
      data: {
        planet: {
          connect: data.planets.map((v: string) => ({
            id: generateId(v)
          }))
        },
        race: {
          connect: data.species.map((s: string) => ({
            id: generateId(s)
          }))
        },
        vehicle: {
          connect: data.vehicles.map((v: string) => ({
            id: generateId(v)
          }))
        },
        starship: {
          connect: data.starships.map((s: string) => ({
            id: generateId(s)
          }))
        }
      }
    });
  }
Enter fullscreen mode Exit fullscreen mode

Creating and Seeding the Database

It's finally time to seed our database with all the data we have. First, we'll need a database to seed into, but Amplication makes that easy. Included by default in the server folder is a file called docker-compose.db.yml with the configuration required to spin up a PostgreSQL instance. When you run the backend locally, it'll be configured to connect to this instance. You must have Docker installed and running. If you need to set up Docker, check out their getting started guide.

With Docker installed and running, execute the following command.

npm run docker:db
Enter fullscreen mode Exit fullscreen mode

Run this command to initialize the database by creating the necessary schemas and seeding the database using the logic we added to the customSeed.ts file.

npm run db:init
Enter fullscreen mode Exit fullscreen mode

Querying the Holocron

Now is the time to see the fruits of your labor. First, let's run the server with the following command.

npm start
Enter fullscreen mode Exit fullscreen mode

After a minute or so, the server should be up and running at localhost:3000, though that URL won't do much. So instead, you should visit localhost:3000/graphql to be greeted by the GraphQL Playground (if you're interested in testing traditional REST API endpoints, visit localhost:3000/api instead).

Clicking "DOCS" and "SCHEMA" on the right side will show you everything you can do and see in the Holocron. Below is a sample query you can try yourself and the query variables you need to get something. Put them in and run the query to see what you get.

# Write your query or mutation here
query people($take: Float) {
  people(take: $take) {
    name
    starships {
      name
    }
    vehicles {
      name
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
{"take": 10}
Enter fullscreen mode Exit fullscreen mode

Executing a GraphQL query.


Executing a GraphQL query.

Wrapping Up

With everything we've done up until now, we've recreated the SWAPI GraphQL Server with the help of Amplication. It's a great intro to GraphQL. Playing with the original SWAPI GrpahQL API was my first experience with GraphQL. I'm happy to share this experience with you!

Remember to join our developer community on Discord; if you like the project and what we're doing, give us a 🌟 on GitHub!

The Amplication Discord. You will never find a more wretched hive of scum an villainy. We must be cautious.

Finally, the source code for this project is available on GitHub.

May the force be with you...

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .