[ PART 1 ] Creating a Twitter clone with GraphQL, Knex, Typescript and React

ipscodingchallenge

ips-coding-challenge

Posted on January 5, 2021

[ PART 1 ] Creating a Twitter clone with GraphQL, Knex, Typescript and React

Hi everyone! I decided to take up the Twitter challenge of the website devchallenges.io. Having so far made only REST API, I wanted to try GraphQL and it's always more fun to have a nice project and design for that ;).

Tweeter challenge

Before launching myself in this challenge which can be rather complex and especially long ;), I still documented myself before and made a project to try to answer the questions I had about GraphQL (Dataloaders, error management ...).

Disclaimer: This is not a guide/tutorial. It's only a way to document my learning and why not get some feedback ;)

Tech Stack

I decided to go on Node.js with ApolloServer + TypeGraphQL + Typescript + Knex with Postgresql for the backend and the frontend will be with React + ApolloClient for the queries. On my last projects Trello clone and Shoppingify I used to do TDD but this time I'll do some tests but it will probably be much lighter.

Here is the Github repository of the project for those who want to follow my progress ;).
Github Repository

Enough talking, let's start coding :D.

yarn add apollo-server graphql type-graphql class-validator knex dotenv pg reflect-metadata
Enter fullscreen mode Exit fullscreen mode
yarn add -D typescript ts-node @types/node nodemon jest
Enter fullscreen mode Exit fullscreen mode

tsconfig.json

{
  "compilerOptions": {
    "target": "es2018",
    "module": "commonjs",                    
    "lib": ["es2018", "esnext.asynciterable"],
    "outDir": "dist",                       
    "rootDir": "src",                       
    "strict": true,                           
    "esModuleInterop": true,                  
    "experimentalDecorators": true,        
    "emitDecoratorMetadata": true,     
    "skipLibCheck": true,                    
    "forceConsistentCasingInFileNames": true
  }
}

Enter fullscreen mode Exit fullscreen mode

package.json

"scripts": {
    "dev": "nodemon src/index.ts --exec ts-node",
    "build": "shx rm -rf dist/ && tsc -p .",
    "start": "node dist/src/index.js"
  },
Enter fullscreen mode Exit fullscreen mode

Creation of the GraphQL server

Once all this is set up, I can finally start my server.

src/index.ts

import 'reflect-metadata'
import { ApolloServer } from 'apollo-server'
import { buildSchema } from 'type-graphql'
import AuthResolver from './resolvers/AuthResolver'

export const createServer = async () => {
  const server = new ApolloServer({
    schema: await buildSchema({
      resolvers: [AuthResolver],
    }),
    context: ({ req, res }) => {
      return {
        req,
        res,
      }
    },
  })

  server.listen().then(({ port }) => {
    console.log(`Listening on port ${port}`)
  })

  return server
}

createServer()

Enter fullscreen mode Exit fullscreen mode

Since I'm going to start by creating users, I start by creating the User entity as well as the AuthResolver.

src/entities/User.ts

import { Field, ID, ObjectType } from 'type-graphql'

@ObjectType()
class User {
  @Field((type) => ID)
  id: number

  @Field()
  username: string

  @Field()
  email: string

  password: string

  @Field()
  created_at: Date

  @Field()
  updated_at: Date
}

export default User

Enter fullscreen mode Exit fullscreen mode

As you can see, my User class does not expose the password field.

src/resolvers/AuthResolver.ts

import { Ctx, Query, Resolver } from 'type-graphql'
import { MyContext } from '../types/types'

@Resolver()
class AuthResolver {
  @Query(() => String)
  async me(@Ctx() ctx: MyContext) {
    return 'Hello'
  }
}

export default AuthResolver
Enter fullscreen mode Exit fullscreen mode

If I test the request, I get my "Hello". So far so good ;).

GraphQL me query

Setting up the database with Knex

To finish this first part, I will set up Knex with a Postgresql database.

knex init -x ts
Enter fullscreen mode Exit fullscreen mode

knexfile.ts


module.exports = {
  development: {
    client: 'pg',
    connection: {
      database: 'challenge_twitter', 
      user: 'postgres',
      password: 'root',
    },
    pool: {
      min: 2,
      max: 10,
    },
    migrations: {
      directory: './src/db/migrations',
    },
    seeds: {
      directory: './src/db/seeds',
    },
  },
  test: {
    client: 'pg',
    connection: {
      database: 'challenge_twitter_test',
      user: 'postgres',
      password: 'root',
    },
    pool: {
      min: 2,
      max: 10,
    },
    migrations: {
      directory: './src/db/migrations',
    },
    seeds: {
      directory: './src/db/seeds',
    },
  },
}

Enter fullscreen mode Exit fullscreen mode

I've created 2 databases for the moment, one for development and the other for testing.

All that's left to do is to create our users table ;).

knex migrate:make create_users_table -x ts
Enter fullscreen mode Exit fullscreen mode

src/db/migrations/create_users_table.ts

import * as Knex from 'knex'

export async function up(knex: Knex): Promise<void> {
  return knex.schema.createTable('users', (t) => {
    t.increments('id')
    t.string('username').notNullable().unique()
    t.string('email').notNullable().unique()
    t.string('password').notNullable()
    t.timestamps(false, true)
  })
}

export async function down(knex: Knex): Promise<void> {
  return knex.raw('DROP TABLE users CASCADE')
}

Enter fullscreen mode Exit fullscreen mode

I start the migration

knex migrate:latest
Enter fullscreen mode Exit fullscreen mode

And all I have to do is create a connection to this database. By the way, I will also install the knex-tiny-logger library to have a simplified visualization of SQL queries ;).

yarn add -D knex-tiny-logger
Enter fullscreen mode Exit fullscreen mode

src/db/connection.ts

import Knex from 'knex'
import KnexTinyLogger from 'knex-tiny-logger'

const config = require('../../knexfile')[process.env.NODE_ENV || 'development']

export default KnexTinyLogger(Knex(config))

Enter fullscreen mode Exit fullscreen mode

All that's left to do is to import this into the index.ts file...

import db from './db/connection'
Enter fullscreen mode Exit fullscreen mode

...and add it to my context to be able to access it from the resolvers.

const server = new ApolloServer({
    schema: await buildSchema({
      resolvers: [AuthResolver],
    }),
    context: ({ req, res }) => {
      return {
        req,
        res,
        db, //Here it is
      }
    },
  })
Enter fullscreen mode Exit fullscreen mode

Here it is for this first part ;). Don't hesitate to tell me if you are interested in me continuing, correcting me when I make mistakes, etc... :D

In the second part, I will setup the test environment.

Have a nice day ;)

You learned 2-3 things and want to buy me a coffee ;)?
https://www.buymeacoffee.com/ipscoding

💖 💪 🙅 🚩
ipscodingchallenge
ips-coding-challenge

Posted on January 5, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related