Chapter 5: Bitcoins API

Full Stack React Native

link Bitcoins API

By the end of this lesson, developers will be able to:

  • Structure full stack applications
  • Create a GraphQL API with pagination

Express API Structure

We are going to build a prompts API with limited CRUD functionality - that means our API will only have the ability to fetch bitcoins.

Let's get started!

Let's begin building our Apollo server by creating a new directory called bitcoins-api:

mkdir bitcoins-api
cd bitcoins-api
npm init -y

link Dependencies

Then, switch into your new directory and install the apollo-server, graphql, sequelize and pg dependencies:

cd prompts-api
yarn add sequelize pg faker && yarn add -D sequelize-cli nodemon

We will also utilize the faker package to add seed data in a few moments.

Next we will initialize a Sequelize project:

npx sequelize-cli init

Let's setup our database configuration:

bitcoins-api/config/config.json

{
  "development": {
    "database": "bitcoins_api_development",
    "dialect": "postgres"
  },
  "test": {
    "database": "bitcoins_api_test",
    "dialect": "postgres"
  },
  "production": {
    "use_env_variable": "DATABASE_URL",
    "dialect": "postgres",
    "dialectOptions": {
      "ssl": true
    }
  }
}

Notice: For production we use use_env_variable and DATABASE_URL. We are going to deploy this app to Heroku. Heroku is smart enough to replace DATABASE_URL with the production database. You will see this at the end of the lesson.

Cool, we should now be all setup to create the database. Let's do that:

npx sequelize-cli db:create

Next we will create a Bitcoins model:

npx sequelize-cli model:generate --name Bitcoins --attributes name:string,symbol:string,price:string,imageUrl:string,favorite:boolean

Checkout the Sequelize Data Types that are available: https://sequelize.org/master/manual/data-types.html

Now we need to execute our migration which will create the bitcoins table in our Postgres database along with the columns:

npx sequelize-cli db:migrate

If you made a mistake, you can always rollback: npx sequelize-cli db:migrate:undo

Now let's create a seed file:

npx sequelize-cli seed:generate --name bitcoins
Let's edit the code for the bitcoins seed file
'use strict';
const faker = require('faker');

const bitcoins = [...Array(100)].map((bitcoin) => (
  {
    name: faker.finance.currencyName(),
    symbol: faker.finance.currencyCode(),
    price: faker.finance.amount(),
    imageUrl: "https://picsum.photos/200/200",
    favorite: false,
    createdAt: new Date(),
    updatedAt: new Date()
  }
))

module.exports = {
  up: (queryInterface, Sequelize) => {
      return queryInterface.bulkInsert('Bitcoins', bitcoins);
  },

  down: (queryInterface, Sequelize) => {
    /*
      Add reverting commands here.
      Return a promise to correctly handle asynchronicity.

      Example:
      return queryInterface.bulkDelete('People', null, {});
    */
  }
};

Seed Database

Execute the seed file:

npx sequelize-cli db:seed:all

Made a mistake? You can always undo: npx sequelize-cli db:seed:undo

Drop into psql and query the database for the demo user:

psql bitcoins_api_development
SELECT * FROM "Bitcoins";

Create a .gitignore file:

touch .gitignore

Insert the following inside .gitignore:

/node_modules
.DS_Store
.env

link GraphQL Server

Let's modify our GraphQL server boilerplate:

const { ApolloServer, gql } = require('apollo-server');
const { Bitcoins, sequelize } = require('../models');

const port = process.env.PORT || 4000;

const typeDefs = `
  type Bitcoin { name: String!, symbol: String!, price: String!, imageUrl: String!, favorite: Boolean }
  type Query { bitcoins(offset: Int, limit: Int): [Bitcoin], favorites: [Bitcoin] }
  type Mutation { addCoin(symbol: String!): Bitcoin, removeCoin(symbol: String!): Bitcoin }
`;

const mapAttributes = (model, { fieldNodes }) => {
  // get the fields of the Model (columns of the table)
  const columns = new Set(Object.keys(model.rawAttributes));
  const requested_attributes = fieldNodes[0].selectionSet.selections.map(
    ({ name: { value } }) => value
  );
  // filter the attributes against the columns
  return requested_attributes.filter(attribute => columns.has(attribute));
};

const resolvers = {
  Query: {
    bitcoins: async (_, { offset = 0, limit = 10 }, context, info) => {
      const bitcoins = await Bitcoins.findAll({
        limit,
        offset,
        order: [['id', 'ASC']],
        attributes: mapAttributes(Bitcoins, info)
      });
      return bitcoins;
    },
    favorites: async (parent, args, context, info) => {
      const bitcoins = await Bitcoins.findAll({
        where: { favorite: true },
        attributes: mapAttributes(Bitcoins, info)
      });
      return bitcoins;
    }
  },
  Mutation: {
    addCoin: async (_, { symbol }, context, info) => {
      const [updated] = await Bitcoins.update(
        { favorite: true },
        {
          where: { symbol: symbol }
        }
      );
      if (updated) {
        const updatedCoin = await Bitcoins.findOne({
          where: { symbol: symbol },
          attributes: mapAttributes(Bitcoins, info)
        });
        return updatedCoin;
      }
      throw new Error('Bitcoin not updated');
    },
    removeCoin: async (_, { symbol }, context, info) => {
      const [updated] = await Bitcoins.update(
        { favorite: false },
        {
          where: { symbol: symbol }
        }
      );
      if (updated) {
        const updatedCoin = await Bitcoins.findOne({
          where: { symbol: symbol },
          attributes: mapAttributes(Bitcoins, info)
        });
        return updatedCoin;
      }
      throw new Error('Bitcoin not updated');
    }
  }
};

const server = new ApolloServer({ 
  typeDefs, 
  resolvers,
});

server.listen({port}, () => console.log(`Server running at http://localhost:${port}`))

Mutations

  • Since we defined a Mutation type that contains a Bitcoin object, we can expect to mutate bitcoins from our database.

  • The addCoin and removeCoin mutations return an updated Bitcoin.

Let's perform our first mutation in the next section.

Test Server

Modify your package.json file to support nodemon. Also, let's create a command npm db:reset that will drop the database, create the database, run the migrations, and seed!

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "start": "nodemon src/index.js",
  "db:reset": "npx sequelize-cli db:drop && npx sequelize-cli db:create && npx sequelize-cli db:migrate && npx sequelize-cli db:seed:all"
},

Let's make sure our server works:

yarn start

Open http://localhost:4000, and paste in the following snippet:

query {
  bitcoins {
    name
    symbol
    price
  }
}

Now let's test our first mutation:

mutation AddCoin($symbol:String!) {
  addCoin(symbol: $symbol) {
    name
    symbol
    price
    imageUrl
    favorite
  }
}

We now have an API with some CRUD functionality built in.

Well Done!

link Deployment

Let's deploy our app to heroku.

First we need to update our package.json:

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "start": "node src/index.js",
  "dev": "nodemon src/index.js",
  "db:reset": "npx sequelize-cli db:drop && npx sequelize-cli db:create && npx sequelize-cli db:migrate && npx sequelize-cli db:seed:all"
},

Make sure you download the heroku cli and run heroku login

  1. heroku create your-heroku-app-name
  2. heroku buildpacks:set heroku/nodejs --app=your-heroku-app-name
  3. heroku addons:create heroku-postgresql:hobby-dev --app=your-heroku-app-name
  4. git status
  5. git commit -am "add any pending changes"
  6. git push heroku master
  7. heroku run npx sequelize-cli db:migrate
  8. heroku run npx sequelize-cli db:seed:all

Having issues? Debug with the Heroku command heroku logs --tail to see what's happening on the Heroku server.

Test the endpoints!

https://your-heroku-app-name.herokuapp.com

Wrapping Up

Today we made and deployed a GraphQL server which displays paginated coins. The next chapter continues with the bitcoin app's network requests.

format_list_bulleted
help_outline