DEV Community

Cover image for Let's Build A Currency Exchange Part I
Marlon Decosta
Marlon Decosta

Posted on • Edited on

Let's Build A Currency Exchange Part I

I started this project with two goals:

  1. Solidify my understanding of GraphQL.

  2. Learn and implement Apollo Server/Client.

I decided to take a deep dive into Apollo, gain a better understanding of its use cases and figure out how to make it play nice with other technologies. In my opinion, the best way to learn a technology is to build something with said tech — then write about it.

We're going to be building a currency exchange with a GraphQL server. We'll use MongoDB/Mongoose to persist our data. We'll implement ApolloServer on the backend and ApolloClient on the front. ApolloClient will provide us with our remote data — React, our local state. Apollo provides an InMemoryCache that we'll utilize on the frontend.

We'll extract our currency data from the Alpha Vantage Finance API. If you are looking for a wider range of options, this review article also covers other stock and currency APIs for you to consider. With this data we'll allow users to buy long, or sell short currency pairs. Later, we'll use Chartjs to implement, well, a chart. 😉

Let's get to work!

We're going to need to install a few dependencies. I'll go over each one in depth as we need them, but for now let's just get them installed.

Create a new project folder and run the following command in your terminal:

  npm init -y
Enter fullscreen mode Exit fullscreen mode

Now that we have a package.json file, let's get to the business of installing our dependencies.

First, let's install nodemon as a dev dependency.

  npm i -D nodemon
Enter fullscreen mode Exit fullscreen mode

Now for the rest:

  npm i apollo-datasource apollo-datasource-rest apollo-server-express bcryptjs express express-session graphql isemail mongoose
Enter fullscreen mode Exit fullscreen mode

Head into package.json, remove the test script and add the following:

  "scripts": {
    "start": "nodemon index.js"
  },
Enter fullscreen mode Exit fullscreen mode

Create an index.js file and add the below code:

  // index.js

  const app = require('express')()

  app.get('/', (req, res) => res.send('Hello world!'))

  const PORT = 4000

  app.listen(PORT, () => console.log(`Server running on port ${PORT}`))
Enter fullscreen mode Exit fullscreen mode

Type npm start into your terminal, then head to localhost:4000. Greeting you should be no other than the ancient, solemn ritual that is, 'Hello World!' With the ritualistic niceties out of the way, let's get to Apollo.

Right now we're just running an express server. Apollo doesn't require us to install express. The reason I've decided to do so is because I'd like to integrate express-session. For this reason, we're utilizing express and apollo-server-express instead of apollo-server.

Head over to Alpha Vantage and grab your API key. It's very simple. Click on the green 'GET YOUR FREE API KEY TODAY' button and you'll be all set.

What is Apollo

The advantages of Apollo will unveil themselves as we begin to work with it. Head to index.js and make the following adjustments:

  // index.js

  const app = require('express')()
  const { ApolloServer } = require('apollo-server-express')

  const typeDefs = require('./typeDefs')
  const resolvers = require('./resolvers') 
  const CurrencyAPI = require('./datasources/currencies')

  const server = new ApolloServer({ 
    typeDefs,
    resolvers,
    dataSources: () => ({
      currencyAPI: new CurrencyAPI() 
    })
  })

  server.applyMiddleware({ app })

  app.listen(PORT, () => {
    console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
  })
Enter fullscreen mode Exit fullscreen mode

We import the ApolloServer class from apollo-server-express and store a new instance in a variable named server. We also import three local files we still have to create. We pass our GraphQL schema (or typeDefinitions) to the typeDefs property on the ApolloServer configuration Object. We do the same for our resolvers and dataSources (this will be explained in detail). Afterwards, we pass app as our lone middleware — for now.

Create a new file named typeDefs.js and add the following:

  // typeDefs.js

  const { gql } = require('apollo-server-express')

  const typeDefs = gql`
    type Query {
      currencyPairInfo(fc: String, tc: String): PairDisplay!
    }

    type PairDisplay {
      fromCurrency: String!
      fromCurrencyName: String
      toCurrency: String!
      toCurrencyName: String
      exchangeRate: String
      lastRefreshed: String
      timeZone: String
      bidPrice: String
      askPrice: String
    }
  `

  module.exports = typeDefs
Enter fullscreen mode Exit fullscreen mode

Unlike a REST API, GraphQL uses only one route. You don't ping different endpoints for each task. Instead, the schema (or typeDefs) describe exactly what data you want and how you want to receive it.

When working with GraphQL there's three things you must understand: Queries, Mutations, and resolvers. Everything revolves around them. You can think of it as GraphQL queries describe how you get data, and GraphQL mutations describe how you mutate (post/put/delete) data. You describe exactly what variables are needed (if any) and what the response should look like. Resolvers are just functions that handle the execution of queries and mutations.

Declare what you want then write the function to do it.

In the query above we're making GraphQL aware that whenever we ask for currencyPairInfo, that two arguments may or may not be provided. After the colon we declare that the response should be returned in the shape described by the type that we name PairDisplay. The exclamation mark at the end declares that this response is required.

I didn't make the arguments to currencyPairInfo required because we're going to set default parameters on the request. We'll set the default parameter for fc (fromCurrency) to EUR and tc (toCurrency) to USD. If we wanted these arguments to be required, we'd simply add an exclamation mark after the type like so: String!.

Let's add our resolvers. Create a new file named resolvers.js and add the following code:

// resolvers.js

const resolvers = {
  Query: {
    currencyPairInfo: async (_, { fc, tc }, { dataSources }) => {
      try {
        const currencyPairs = await dataSources.currencyAPI.getCurrencyPair(fc, tc)
        return currencyPairs
      } catch (error) { throw err }
    }
  }
}

module.exports = resolvers
Enter fullscreen mode Exit fullscreen mode

In GraphQL, resolvers have access to the context. The context is an Object shared by all resolvers. It's useful for keeping track of things such as authentication info, the current user, database connections, and data sources. The context is available as the third argument of each resolver.

A resolvers function signature:

  1. First argument = parent.
  2. Second argument = args.
  3. Third argument = context.

It's considered best practice to keep your resolvers clean and concise, so we abstract the heavy lifting out to another file. This is the file we imported into index.js and still need to create. Data sources get access to the GraphQL context. This is why we don't have to import it into resolvers.js. We just destructor it from the context Object.

Create a new folder named datasources. Inside make a new file and name it currencies.js. Add the below code:

// currencies.js

const { RESTDataSource } = require('apollo-datasource-rest') 
const keys = require('../config/keys')

class CurrencyAPI extends RESTDataSource {
  constructor() {
    super() 
    this.baseURL = ''
  }

  async getCurrencyPair(fc='EUR', tc='USD') {
    try {
      const data = await this.get(`https://www.alphavantage.co/query?
function=CURRENCY_EXCHANGE_RATE&from_currency=${fc}
&to_currency=${tc}&apikey=${keys.alphaVantageAPIKey}`),
            response = data['Realtime Currency Exchange Rate'],
            fromCurrency = response['1. From_Currency Code'],
            fromCurrencyName = response['2. From_Currency Name'],
            toCurrency = response['3. To_Currency Code'],
            toCurrencyName = response['4. To_Currency Name'],
            exchangeRate = response['5. Exchange Rate'],
            lastRefreshed = response['6. Last Refreshed'],
            timeZone = response['7. Time Zone'],
            bidPrice = response['8. Bid Price'],
            askPrice = response['9. Ask Price']
      return data && response && {
          fromCurrency,
          fromCurrencyName,
          toCurrency,
          toCurrencyName,
          exchangeRate,
          lastRefreshed,
          timeZone,
          bidPrice,
          askPrice
        }
    } catch (err) { throw err }
  }
}

module.exports = CurrencyAPI
Enter fullscreen mode Exit fullscreen mode

We import RESTDataSource from apollo-datasource-rest. We extend this class (create a child class) to define our data source. An Apollo data source is a class that encapsulates all of the data fetching logic, as well as caching and deduplication for a particular service.

From the docs:

The Apollo RESTDataSource also sets up an in-memory cache that caches responses from our REST resources with no additional setup. We call this partial query caching. What's great about this cache is that you can reuse existing caching logic that your REST API exposes. Apollo REST data sources also have helper methods that correspond to HTTP verbs like GET and POST.

We'll discuss this cache in more detail once we get to ApolloClient.

All this file does is fetch some data from the Alpha Vantage API. We extend the RESTDataSource class and in our contructor function we initialize our baseURL. baseURL is given to us curtesy of Apollo. A simple example of how this is useful, is if we had two methods in this class that had to hit separate endpoints of the same URL.

For instance:

  constructor() {
    super()
    this.baseURL = 'https://github.com/'
  }

  // Later in some method
  this.get('marlonanthony') // https://github.com/marlonanthony

  // In some other method
  this.get('peggyrayzis') // https://github.com/peggyrayzis
Enter fullscreen mode Exit fullscreen mode

You can also set URL's dynamically. Let's take a look at an example from the docs:

  get baseURL() {
    if (this.context.env === 'development') {
      return 'https://movies-api-dev.example.com/';
    } else {
      return 'https://movies-api.example.com/';
    }
}
Enter fullscreen mode Exit fullscreen mode

After our constructor function we implement the method we called in our resolver, getCurrencyPair. This method is responsible for fetching our realtime currency exchange rate data. We utilize the URL given to us by Alpha Vantage, add our arguments and our API key.

The Alpha Vantage API is free, which means convienent. That said, their naming conventions are a bit goofy and require us to use bracket notation, hence the verbosity.

By default Apollo Server supports GraphQL Playground. The Playground is an interactive, in-browser GraphQL IDE for exploring your schema and testing your queries/mutations. Think Postman but for GraphQL.

Start your server with npm start. Then head to localhost:4000/graphql and take a look.

currenyPairInfo query

On the left side of the play button we declare that we want to query some data. We then explain which query and provide the necessary arguments. If you press control + spacebar (on Mac), you should get autocomplete suggestions for your schema. Afterwards, we declare what data we want returned. Once you press the play button you'll see the response on the right half of the playground.

Inside of our getCurrencyPair method, we define everything that is possible to return from this query. The difference between GraphQL and REST is that if we wished, we could limit our request to any slice of this data we like.

Sweet! We're fetching realtime currency exchange rates from the Alpha Vantage API! That said, we're not done here. I stated earlier that we'd be implementing a chart to display a monthly time series of currency pair data. To do this we need to add another method to our CurrencyAPI class.

  // currencies.js

  async getMonthlyTimeSeries(fc='EUR', tc='USD') {
    try {
      const data = await this.get(`https://www.alphavantage.co/query?
function=FX_MONTHLY&from_symbol=${fc}&to_symbol=${tc}&apikey=${keys.alphaVantageAPIKey}`),
            timeSeries = data && data['Time Series FX (Monthly)'],
            timesArray = timeSeries && Object.keys(timeSeries).reverse(),
            valuesArray = timeSeries && Object.values(timeSeries).map(val => val['4. close']).reverse()

      return { timesArray, valuesArray }

    } catch (error) { throw error }
  }
Enter fullscreen mode Exit fullscreen mode

Here we utilize a different Alpha Vantage endpoint. We provide the arguments and API key as we did before. We return an object containing two arrays, the timesArray (x-axis) and the valuesArray (y-axis). This is all we need for our chart.

We need to make a resolver to call this method and add a query to our typeDefs. Head into typeDefs.js and adjust the query type to the following:

// typeDefs.js 

  type Query {
    currencyPairInfo(fc: String, tc: String): PairDisplay!
    monthlyTimeSeries(fc: String, tc: String): TimeSeries!
  }
Enter fullscreen mode Exit fullscreen mode

Here we expect to receive a fromCurrency (fc) and toCurrency (tc) argument. Again, we don't make the arguments required because we just set default parameters on the request. The reason I chose to do this is so that when a person navigates to the chart, the page will load with data instead of being blank until the user enters a currency pair.

Our monthlyTimeSeries query requires us to return data of the type TimeSeries. Let's define exactly what this is. Add the following type to typeDefs.js:

// typeDefs.js 

  type TimeSeries {
    timesArray: [String!]!
    valuesArray: [String!]!
  }
Enter fullscreen mode Exit fullscreen mode

Here we declare that two arrays must be returned and that those arrays must be filled with Strings. Both the string and the arrays are required (!).

Lastly, let's add our resolver. Adjust resolvers.js such that it resembles the following:

// resolvers.js

  const resolvers = {
    Query: {
      currencyPairInfo: async (_, { fc, tc }, { dataSources }) => {
        try {
          const currencyPairs = await dataSources.currencyAPI.getCurrencyPair(fc, tc)
          return currencyPairs
        } catch (error) { throw err }
      },
      monthlyTimeSeries: async (_, { fc, tc }, { dataSources }) => {
        try {
          const timeSeries = await dataSources.currencyAPI.getMonthlyTimeSeries(fc, tc)
          return timeSeries
        } catch (error) { throw error }
      }
    }
  }

module.exports = resolvers
Enter fullscreen mode Exit fullscreen mode

Open up GraphQL Playground and query monthlyTimeSeries.

montly time series

The GraphQL pattern should be becoming clear by now.

  • Create a query/mutation.
  • Create a resolver to handle said query/mutation.

And like that we're done with the Alpha Vantage API!

We're slowly getting familiar with GraphQL and Apollo. Let's get a little more comfortable and tackle authentication. Handling authentication/authorization is a well-covered topic. We'll simply focus on integration with Apollo.

The first thing we should do is create a database. We'll be using MongoDB/Mongoose. Head to MongoDB Atlas and sign up/sign in. Creating a remote database with Atlas is fairly straightforward. Once you login, click on the 'New Project' button. From here just choose your cloud provider of choice, select your region, and name your cluster. Once your cluster is built, click the connect button. Whitelist your IP address and create an admin user for the project. Choose the 'Connect Your Application' option and copy the connection string provided. Finally, click on the 'collections' button. This is where we'll see our data.

Replace <password> in your connection string with your user password but store it in a variable and place it in either an env file or a config folder. As long as you don't push it to GitHub.

Let's connect to our database and define our user schema. Back in index.js import mongoose, import your MongoDB password, then adjust index.js to the following:

// index.js

const app = require('express')()
const { ApolloServer } = require('apollo-server-express')
const mongoose = require('mongoose')

const typeDefs = require('./typeDefs')
const resolvers = require('./resolvers') 
const CurrencyAPI = require('./datasources/currencies')
const { mongoPassword } = require('./config/keys')

const server = new ApolloServer({ 
  typeDefs,
  resolvers,
  dataSources: () => ({
    currencyAPI: new CurrencyAPI() 
  })
})

server.applyMiddleware({ app })

mongoose
.connect(`mongodb+srv://marlon:${mongoPassword}@cluster0-o028g.mongodb.net/forex?retryWrites=true&w=majority`, { useNewUrlParser: true })
.then(() => app.listen(4000, () => {
  console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
})).catch(err => console.log(err)) 
Enter fullscreen mode Exit fullscreen mode

You'll notice that at the end of the URL we added a bit of configuration to rid ourselves of that pesky MongoDB/Mongoose warning. Once you save index.js we'll be connected to our database.

Now for the schema. Create a folder named models. Inside of models create a new file named User.js and insert the following:

// User.js

const mongoose = require('mongoose') 
const Schema = mongoose.Schema

const User = new Schema({
  email: {
    type: String,
    required: true 
  },
  password: {
    type: String,
    required: true 
  },
  name: {
    type: String,
    required: true 
  },
  bankroll: {
    type: Number,
    default: 1000000,
    required: true
  },
  pairs: [
    {
      type: Schema.Types.ObjectId,
      ref: 'Pair'
    }
  ]
}, {
  timestamps: true
})

module.exports = mongoose.model('User', User)
Enter fullscreen mode Exit fullscreen mode

We import the Schema class from mongoose and create a new instance with which we name User. Afterwards, we define our schema. Each user will have an ID gifted to them by MongoDB so we need not define it. Users will have to provide an email, password, and name. We start each user off with a million bucks — because we can. Each user will want to track which currency pair positions they've opened. We assign a pairs property that will provide us an array of IDs for each pair a user opens. Finally, by adding timestamps: true to our schema, Mongoose provides us with two properties: createdAt and updatedAt.

Create a new file in the models folder and name it Pair.js. Inside write the following code:

// Pair.js

const mongoose = require('mongoose') 
const Schema = mongoose.Schema

const Pair = new Schema({
  user: {
    type: Schema.Types.ObjectId,
    ref: 'User'
  },
  pair: {
    type: String,
    required: true 
  },
  lotSize: {
    type: Number,
    required: true 
  },
  position: {
    type: String,
    required: true 
  },
  openedAt: {
    type: Number,
    required: true 
  },
  closedAt: {
    type: Number,
  },
  pipDif: {
    type: Number,
  },
  profitLoss: {
    type: Number
  },
  open: {
    type: Boolean,
    required: true,
    default: false
  }
}, {
  timestamps: true
})

module.exports = mongoose.model('Pair', Pair) 
Enter fullscreen mode Exit fullscreen mode

We store the users ID in a property called user. The pair property will look something like this: EUR/USD. lotSize is the amount of money the user placed on the position. position is either 'long' or 'short.' pipDif will be explained in detail later but for now just know that it's how we'll calculate the relative difference in value between a currency pair, and therefore the profit/loss of a position. open informs us whether or not the position has been closed.

Open up typeDefs.js and add two types: User and Pair.

// typeDefs.js

  type User {
    id: ID!
    email: String!
    name: String!
    bankroll: Float!
    pairs: [Pair]
    createdAt: String!
    updatedAt: String!
  }

  type Pair {
    id: ID!
    user: ID!
    pair: String!
    lotSize: Int!
    position: String!
    openedAt: Float!
    closedAt: Float
    pipDif: Float
    profitLoss: Float
    open: Boolean!
    createdAt: String!
    updatedAt: String!
  }
Enter fullscreen mode Exit fullscreen mode

For the most part, if something is required in your model schema, then it should probably be required in your GraphQL schema.

Time to add our first mutation. Inside typeDefs.js add the Mutation type.

// typeDefs.js

  type Mutation {
    register(email: String!, password: String!, name: String!): Boolean!
  }
Enter fullscreen mode Exit fullscreen mode

The user must submit an email, password, and their name. We return true or false depending on the success of a users registration.

We've handled the typeDefs, now for the resolver. We'll need to add a Mutation property to our resolvers Object.

// resolvers.js

const resolvers = {
  Query: {
    currencyPairInfo: async (_, { fc, tc }, { dataSources }) => {
      try {
        const currencyPairs = await dataSources.currencyAPI.getCurrencyPair(fc, tc)
        return currencyPairs
      } catch (error) { throw err }
    },
    monthlyTimeSeries: async (_, { fc, tc }, { dataSources }) => {
      try {
        const timeSeries = await dataSources.currencyAPI.getMonthlyTimeSeries(fc, tc)
        return timeSeries
      } catch (error) { throw error }
    }
  },

  Mutation: {
    register: async (_, { email, password, name }, { dataSources }) => {
      try {
        const newUser = await dataSources.userAPI.createNewUser({ email, password, name })
        return newUser
      } catch (error) { throw error }
    },
  }
}

module.exports = resolvers
Enter fullscreen mode Exit fullscreen mode

Again we keep our resolvers clean and abstract the heavy lifting to another file. But what file? RESTDataSource is responsible for fetching data from a REST API. This is not what we're doing here. Apollo allows us to create custom data sources with the generic apollo-datasource package. This is what we'll be using.

Create a new file in the datasources folder and name it user.js.

// user.js

const { DataSource } = require('apollo-datasource')
const { UserInputError } = require('apollo-server-express')
const isEmail = require('isemail')
const bcrypt = require('bcryptjs')

const User = require('../models/User') 

class UserAPI extends DataSource {

  // gain access to the GraphQL context
  initialize(config) {
    this.context = config.context
  }

  async createNewUser({ email, password, name }) {
    try {
      if(!isEmail.validate(email)) { throw new UserInputError('Invalid Email!') }
      const existingUser = await User.findOne({ email })
      if(existingUser) { throw new UserInputError('User already exist!') }
      const hashedPassword = await bcrypt.hash(password, 12)
      const user = await new User({
        name,
        email,
        password: hashedPassword
      })
      await user.save()
      return true 
    } catch (error) { throw error }
  }
}

module.exports = UserAPI
Enter fullscreen mode Exit fullscreen mode

First, we import Apollo's DataSource class. We then create a subclass by extending DataSource and name it UserAPI. Apollo grants us access to the context from inside this class by adding the initialize function. This is a function that gets called by ApolloServer when being setup. This function gets called with the datasource config including things like caches and context. This allows us to utilize this.context, granting us access to the request context, so we can know about the user making requests.

We also import UserInputError from apollo-server-express. This allows us to differentiate between error types. Apollo Client distinguishes two kinds of errors: graphQLErrors and networkError. Let's take a look at a blog post written by the Apollo team last year.

graphQLError vs networkError

What about these graphQLErrors thrown in our resolvers? Again, let's take a look at this blog post.

resolver errors

We import isemail to ensure a valid email was provided. We also import bcrypt to hash user passwords before saving them to the database. Lastly, we import our User schema.

Head to index.js and import our newly created data source. Then add a new instance of our UserAPI class to ApolloServer's configuration Object:

// index.js

const UserAPI = require('./datasources/user')

const server = new ApolloServer({ 
  typeDefs,
  resolvers,
  dataSources: () => ({
    currencyAPI: new CurrencyAPI(),
    userAPI: new UserAPI()
  })
})
Enter fullscreen mode Exit fullscreen mode

Save your files and take a look at the GraphQL Playground.

register mutation

If you try to register the same user twice, you should get the UserInputError we defined earlier ("User already exist!"). You should also be able to see our newly created user in the database. Now that we can register users, let's get them logged in.

We'll be using express-session to keep track of our user. The idea is that once a user successfully logs in, we'll attach the users id to the session on the request Object. We'll gain access to the request Object curtesy of the context Object in our resolvers, or via this.context in our UserAPI Class — once we place it on the context.

Head to index.js and make the following adjustments:

// index.js

const app = require('express')()
const { ApolloServer } = require('apollo-server-express')
const mongoose = require('mongoose')
// Import express-session
const session = require('express-session')

const typeDefs = require('./typeDefs')
const resolvers = require('./resolvers') 
const CurrencyAPI = require('./datasources/currencies')
const UserAPI = require('./datasources/user')
// import your session secret
const { mongoPassword, secret } = require('./config/keys') 

const server = new ApolloServer({ 
  typeDefs,
  resolvers,
  dataSources: () => ({
    currencyAPI: new CurrencyAPI(),
    userAPI: new UserAPI()
  }),
  // add req Object to context
  context: ({ req }) => ({ req })
})

// add express-session to middleware
app.use(session({
  secret,
  resave: false,
  saveUninitialized: false
}))

// add cors to middleware
server.applyMiddleware({ 
  app, 
  cors: {
      credentials: true,
      origin: 'http://localhost:3000'
  }
})

mongoose
.connect(`mongodb+srv://marlon:${mongoPassword}@cluster0-o028g.mongodb.net/forex?retryWrites=true&w=majority`, { useNewUrlParser: true })
.then(() => app.listen(4000, () => {
  console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
})).catch(err => console.log(err)) 
Enter fullscreen mode Exit fullscreen mode

Import express-session then create and import your session secret. Any String will do. Then add the request Object to the context and pass our express-session and cors middleware.

Let's add login to our typeDefs.

// typeDefs.js

type Mutation {
  register(email: String!, password: String!, name: String!): Boolean!
  login(email: String!, password: String!): User
}
Enter fullscreen mode Exit fullscreen mode

The login resolver:

// resolvers.js 

Mutation: {
  register: async (_, { email, password, name }, { dataSources }) => {
    try {
      const newUser = await dataSources.userAPI.createNewUser({ email, password, name })
      return newUser
    } catch (error) { throw error }
  },
  login: async (_, { email, password }, { dataSources }) => {
    try {
      const user = await dataSources.userAPI.loginUser({ email, password })
      return user 
    } catch (error) { throw error }
  },
}
Enter fullscreen mode Exit fullscreen mode

Head to datasources/user.js and add a method named loginUser to the UserAPI class.

// datasources/user.js

async loginUser({ email, password }) {
  try {
    if (!isEmail.validate(email)) { throw new UserInputError('Invalid Email') }
    const user = await User.findOne({ email }) 
    if(!user) { throw new UserInputError('Email or password is incorrect!') }
    const isEqual = await bcrypt.compare(password, user.password)
    if(!isEqual) { throw new UserInputError('Email or password is incorrect!') }
    this.context.req.session.userId = user.id 
    return user 
  } catch (error) { throw error }
}
Enter fullscreen mode Exit fullscreen mode

We validate the email with isemail. Next, we compare the given password with the hashed password from the database. If all goes well, we place a userId property on req.session. This is how we'll keep track of our user. I'm returning the entire user Object here for the sake of simplicity. That said, in a production app you'd never want to return the users password.

Head to the GraphQL Playground and run the login mutation.

login mutation

The way we'll keep track of the user on the frontend is with a me query. This me query will tell us which user is attempting to perform an action, hence allowing us to decide whether this user has authorization to perform said action.

Let's get to it!

First, add the me query to typeDefs.js.

// typeDefs.js

type Query {
  currencyPairInfo(fc: String, tc: String): PairDisplay!
  monthlyTimeSeries(fc: String, tc: String): TimeSeries!
  me: User
}
Enter fullscreen mode Exit fullscreen mode

Add me query to the resolvers Query Object.

// resolvers.js 

me: async (_, __, { dataSources }) => {
  try {
    const user = await dataSources.userAPI.getMe()
    return user
  } catch (error) { throw error }
},
Enter fullscreen mode Exit fullscreen mode

Next, we need to add the getMe method to our data source. Head to datasources/user.js and add the following:

// datasources/user.js

  async getMe() {
    try {
      if(!this.context.req.session.userId) return null 
      const user = await User.findById(this.context.req.session.userId) 
      return user 
    } catch (error) { throw error }
  }
Enter fullscreen mode Exit fullscreen mode

Now head back to the GraphQL Playground. Click on the settings gear icon at the top right of the playground and adjust "request.credentials" to: "request.credentials":"include". Log in then execute the me query and you should get back the logged in user.

me query

Now log in with a different user and when you perform the me query, it'll provide the new users info. This is because on every request a new context is being built. Therefore, req.session.userId will always belong to the user that made the request.

Sweet! This is a good time to create a logout mutation. Let's get to it! Head to typeDefs.js and add the logout mutation.

// typeDefs.js

type Mutation {
  register(email: String!, password: String!, name: String!): Boolean!
  login(email: String!, password: String!): User!
  logout: Boolean
}
Enter fullscreen mode Exit fullscreen mode

Add logout to the Mutation Object in resolvers.js.

// resolvers.js

Mutation: {
  register: async (_, { email, password, name }, { dataSources }) => {
    try {
      const newUser = await dataSources.userAPI.createNewUser({ email, password, name })
      return newUser
    } catch (error) { throw error }
  },
  login: async (_, { email, password }, { dataSources }) => {
    try {
      const user = await dataSources.userAPI.loginUser({ email, password })
      return user 
    } catch (error) { throw error }
  },
  logout: async (_, __, { req }) => {
    try { req.session.destroy(() => false) } 
    catch (error) { throw error }
  },
}
Enter fullscreen mode Exit fullscreen mode

When a user clicks logout we destroy the session and return false. When you perform the logout Mutation you should get back null.

Behold! A user can log out!

Being this is a currency exchange, it'd probably be best if we allow users to exchange currency. 🤔 Open up typeDefs.js and add the openPosition mutation.

// typeDefs.js

type Mutation {
  register(email: String!, password: String!, name: String!): Boolean!
  login(email: String!, password: String!): User!
  logout: Boolean
  openPosition(pair: String!, lotSize: Int, openedAt: Float!, position: String!): PairUpdateResponse!
}
Enter fullscreen mode Exit fullscreen mode

Now add PairUpdateResponse to typeDefs.js:

// typeDefs.js

type PairUpdateResponse {
  success: Boolean!
  message: String!
  pair: Pair!
}
Enter fullscreen mode Exit fullscreen mode

When a user attempts to open a positon (buy/sell a currency pair), they'll get back a success response (true/false), a message describing the action taken, and information about the pair.

Add the openPosition mutation to resolvers.js.

// resolvers.js

openPosition: async (_, { pair, lotSize, openedAt, position }, { dataSources }) => {
  try {
    const open = await dataSources.userAPI.newPosition({ 
      pair, 
      lotSize, 
      openedAt, 
      position 
    })
    return open 
  } catch (error) { throw error }
},
Enter fullscreen mode Exit fullscreen mode

openPosition takes a few arguments. pair will look something like: 'EUR/USD'. lotSize is the size of the position you're taking (how much money you're placing on the position). openedAt is the price that you bought/sold at. position will be either 'long' or 'short' depending on if the user wants to buy long (bet the price will go up) or sell short (bet the price will go down).

Add the newPosition method to datasources/user.js, but first import AuthenticationError and ForbiddenError from apollo-server-express. We'll also need to import our Pair schema.

// datasources/user.js

const { 
  UserInputError, 
  AuthenticationError, 
  ForbiddenError 
} = require('apollo-server-express')

const Pair = require('../models/Pair')

async newPosition({ pair, lotSize, openedAt, position }) {
  try {
    const user = await User.findById(this.context.req.session.userId)
    if(!user) throw new AuthenticationError('Invalid Crendentials!')
    if(user.bankroll < lotSize) throw new ForbiddenError(`Insufficient funds!`)

    const newPair = new Pair({
      pair,
      lotSize,
      openedAt,
      position,
      open: true,
      user: this.context.req.session.userId
    })
    const pairResult = await newPair.save()
    user.pairs.unshift(pairResult)
    user.bankroll -= lotSize
    await user.save()
    const message = `Congrats ${user.name}! You've opened a ${position} position on ${pair} at ${openedAt}!`
    const success = true
    return { success, message, pair: pairResult }
  } catch (error) { throw error }
}
Enter fullscreen mode Exit fullscreen mode

First we check if the user has enough money to complete the transaction. If they do we create the pair and add it to the pairs array. We subtract the position size from the users bankroll, and return a response in the shape of PairUpdateResponse.

Open up GraphQL Playground, login and run the openPosition mutation.

openPosition

Now that our users can open a position, it might be a good idea to provide a way of closing said position. Let's add a closePosition mutation to typeDefs.js.

// typeDefs.js

type Mutation {
  register(email: String!, password: String!, name: String!): Boolean!
  login(email: String!, password: String!): User!
  logout: Boolean
  openPosition(pair: String!, lotSize: Int, openedAt: Float!, position: String!): PairUpdateResponse!
  closePosition(id: ID!, closedAt: Float!): PairUpdateResponse!
}
Enter fullscreen mode Exit fullscreen mode

The closePosition mutation takes as arguments the pair id and the exit price (closedAt). It then returns a response in the form of PairUpdateResponse.

Let's handle the resolver.

// resolvers.js

closePosition: async(_, { id, closedAt }, { dataSources }) => {
  try {
    const close = await dataSources.userAPI.exitPosition({ id, closedAt })
    return close 
  } catch (error) { throw error }
},
Enter fullscreen mode Exit fullscreen mode

Back to datasource/user.js to implement the exitPosition method.

// datasources/user.js

async exitPosition({ id, closedAt }) {
  try {
    const user = await User.findById(this.context.req.session.userId) 
    if(!user) throw new AuthenticationError('Invalid credentials!')

    const pair = await Pair.findById(id) 
    if(!pair) throw new AuthenticationError('Invalid credentials!')
    if(!pair.open) throw new ForbiddenError('Transaction already complete!')
    let pipDifFloat
    pair.position === 'long' 
      ? pipDifFloat = (closedAt - pair.openedAt).toFixed(4) 
      : pipDifFloat = (pair.openedAt - closedAt).toFixed(4)   
    pair.pipDif = pipDifFloat
    pair.closedAt = closedAt
    pair.profitLoss = pipDifFloat * pair.lotSize
    pair.open = false 
    const savedPair = await pair.save()

    user.bankroll += (pair.lotSize + savedPair.profitLoss) 
    await user.save() 

    const success = true 
    const message = `${ savedPair.profitLoss > 0 
      ? 'Congrats!' 
      : ''
      } ${user.name} you've closed your ${savedPair.position} position on ${savedPair.pair} at ${closedAt}${ savedPair.profitLoss > 0 
      ? '! For a profit of '+Math.round(savedPair.profitLoss)+'!' 
      : '. For a loss of '+Math.round(savedPair.profitLoss)+'.'}`
    return { success, message, pair: savedPair }
  }
  catch (error) { throw error }
}
Enter fullscreen mode Exit fullscreen mode

Once we find our pair we create a variable named pipDifFloat. If the position is long, we subtract the openedAt price from the closedAt price. If the position is short, we subtract the closedAt price from the openedAt price. We store the result in pipDifFloat then set the pairs pipDif property to pipDifFloat.

Next, we set the closedAt price and calculate the profitLoss by multiplying the pipDifFloat by the lotSize. Afterwards, we set the open property to false and save our pair. Once we save the pair we adjust the users bankroll accordingly. finally, we return PairUpdateResponse and give the user the good/bad news.

Take a look at the GraphQL Playground:

closePosition

We're making some serious progress. Let's make some more!

We have two related queries left so let's tackle them together. Inside typeDefs.js adjust the Query type to the following:

// typeDefs.js

type Query {
  currencyPairInfo(fc: String, tc: String): PairDisplay!
  monthlyTimeSeries(fc: String, tc: String): TimeSeries!
  me: User
  findPair(id: ID!): Pair!
  getPairs: [Pair!]
}
Enter fullscreen mode Exit fullscreen mode

One query to get a pair by id. Another query to retreive all of a users pairs. Let's take care of the resolvers. Adjust the Query Object such that it resembles the below code:

// resolvers.js

Query: {
  currencyPairInfo: async (_, { fc, tc }, { dataSources }) => {
    try {
      const currencyPairs = await dataSources.currencyAPI.getCurrencyPair(fc, tc)
      return currencyPairs
    } catch (error) { throw err }
  },
  monthlyTimeSeries: async (_, { fc, tc }, { dataSources }) => {
    try {
      const timeSeries = await dataSources.currencyAPI.getMonthlyTimeSeries(fc, tc)
      return timeSeries
    } catch (error) { throw error }
  },
  me: async (_, __, { dataSources }) => {
    try {
      const user = await dataSources.userAPI.getMe()
      return user
    } catch (error) { throw error }
  },
  findPair: async (_, { id }, { dataSources }) => {
    try {
      const foundPair = await dataSources.userAPI.getPair({ id })
      return foundPair
    } catch (error) { throw error }
  },
  getPairs: async (_, __, { dataSources }) => {
    try {
      const foundPairs = await dataSources.userAPI.findPairs()
      return [...foundPairs]
    } catch (error) { throw error }
  },
},
Enter fullscreen mode Exit fullscreen mode

On to datasources/user.js to define getPair and findPairs.

// datasources/user.js

async getPair({ id }) {
  try {
    const pair = await Pair.findById(id)
    if(!pair || pair.user.toString() !== this.context.req.session.userId) { 
      throw new AuthenticationError('Invalid credentials!') 
    } 
    return pair
  } catch (error) { throw error }
}

async findPairs() {
  try {
    const pairs = await Pair
      .find({ user: this.context.req.session.userId })
      .sort({ updatedAt: -1 })
    if(!pairs.length) throw new UserInputError('Nothing to show!')
    return [...pairs] 
  } catch (error) { throw error }
}
Enter fullscreen mode Exit fullscreen mode

You should see something similar in the GraphQL Playground:

findPair

getPairs

One last mutation and we're done with the backend! Our final specimen — addFunds. Users will want to add money to their account. Far be it from us to leave them wanting.

We'll start in typeDefs.js. Create the addFunds mutation and define its response type — AddFundsResponse.

// typeDefs.js

type Mutation {
  register(email: String!, password: String!, name: String!): Boolean!
  login(email: String!, password: String!): User!
  logout: Boolean
  openPosition(pair: String!, lotSize: Int, openedAt: Float!, position: String!): PairUpdateResponse!
  closePosition(id: ID!, closedAt: Float!): PairUpdateResponse!
  addFunds(amount: Int!): AddFundsResponse!
}

  type AddFundsResponse {
    success: Boolean!
    message: String!
    user: User!
  }
Enter fullscreen mode Exit fullscreen mode

addFunds takes amount as a lone argument because we already know about the user via the context. Let's tackle our last resolver. Once we implement addFunds, our Mutation Object should resemble the following:

// resolvers.js

Mutation: {
  register: async (_, { email, password, name }, { dataSources }) => {
    try {
      const newUser = await dataSources.userAPI.createNewUser({ email, password, name })
      return newUser
    } catch (error) { throw error }
  },
  login: async (_, { email, password }, { dataSources }) => {
    try {
      const user = await dataSources.userAPI.loginUser({ email, password })
      return user 
    } catch (error) { throw error }
  },
  logout: async (_, __, { req }) => {
    try { req.session.destroy(() => false) }
    catch (error) { throw error }
  },
  openPosition: async (_, { pair, lotSize, openedAt, position }, { dataSources }) => {
    try {
      const open = await dataSources.userAPI.newPosition({ pair, lotSize, openedAt, position })
      return open 
    } catch (error) { throw error }
  },
  closePosition: async(_, { id, closedAt }, { dataSources }) => {
    try {
      const close = await dataSources.userAPI.exitPosition({ id, closedAt })
      return close 
    } catch (error) { throw error }
  },
  addFunds: async (_, { amount }, { dataSources }) => {
    try {
      const weeee = await dataSources.userAPI.additionalFunds({ amount })
      return weeee
    } catch (error) { throw error }
  }
}
Enter fullscreen mode Exit fullscreen mode

On to datasources/user.js:

// datasources/user.js

async additionalFunds({ amount }) {
  try {
    const user = await User.findById(this.context.req.session.userId)
    if(!user) throw new AuthenticationError('Invalid credentials!')
    user.bankroll += amount 
    const savedUser = await user.save()
    const success = true
    const message = `Congrats ${user.name} you've added ${amount} to your bankroll!`
    return { bankroll: savedUser.bankroll, success, message } 
  } catch (error) { throw error }
}
Enter fullscreen mode Exit fullscreen mode

Your GraphQL Playground should look something like this:

addFunds

Behold! We're done with the backend! The frontend awaits!

The code for this project is on my GitHub.

Reach out: Twitter | Medium | GitHub

Top comments (1)

Collapse
 
bhavinvirani profile image
BHAVIN VIRANI

excited🤩 to learn something new from this article.