Remote Typescript Developer : GraphQL, TypeScript and PostgreSQL API

The go-to stack in recent times for developers, especially Remote Typescript Developers, revolves around GraphQl and Typescript. While I have utilized Vanilla JavaScript in a recent project, my experience with Typescript has been extensive. Despite not having direct hands-on experience initially, I found great assistance through a tutorial that guided me effectively. In this article, I aim to share the insights gained and guide others through the process. Before delving into the details, let’s explore:

This image has an empty alt attribute; its file name is nnnutbfl.jpg

Why GraphQL, TypeScript and PostgreSQL ?

The description in our API is provided by GraphQL. It helps in understanding the needs of the clients and helps us while dealing with large amounts of data, as one can have all the data by running only one query.

Typescript is used as a superset of javascript. When javascript code takes more compliance time and becomes messier to reuse or maintain we can use typescript instead.

PostgreSQL is based on personal preference and is open-source. you can view the following link for more details.

https://www.compose.com/articles/what-postgresql-has-over-other-open-source-sql-databases/

Preconditions

  1. yarn NPM can be used
  2. node: v.10 or superior
  3. PostgreSQL = 12
  4. basic typescript knowledge

Structure of folder

project is structured in the following way:

graphql_api/
       ...
        dist/
          bundle.js
        src/
         database/
              knexfile.ts
              config.ts
              migrations/
              models/
                User.ts
                Pet.ts
          __generated__/
          schema/
              resolvers/
                  user.ts
                  pet.ts
                  index.ts

              graphql/
                  schema.ts
              index.ts/
          index.ts 

Dependencies

  • Apollo server: it is an open-source Graphsql server maintained by the community. It works by using node.js and HTTP frameworks.
  • Objection: Sequelize can also be used but objection.js is better because it is an ORM that embraces SQL.

Development

  • Webpack: Webpack can be used to compile JavaScript modules, node.js do not accept files like .gql or .graphql, that’s why we use Webpack. install the following
yarn add graphql apollo-server-express express body-parser objection pg knex

and some dependencies of dev:

yarn add -D typescript @types/graphql @types/express @types/node  graphql-tag concurrently nodemon ts-node webpack webpack-cli webpack-node-external

Configuration

use command tsconfig

{
  "compilerOptions": {
  "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
                      /* Concatenate and emit output to single file. */
     "outDir": "dist",                        /* Redirect output structure to the directory. */
     "rootDir": "src",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
   
    "strict": true,                           /* Enable all strict type-checking options. */
 
     "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
  
 "skipLibCheck": true,                     /* Skip type checking of declaration files. */
    "forceConsistentCasingInFileNames": true  /* Disallow inconsistently-cased references to the same file. */
  },
  "files": ["./index.d.ts"]
}

Webpack

const path = require('path');
const {CheckerPlugin} = require('awesome-typescript-loader');
var nodeExternals = require('webpack-node-externals');

module.exports = {
  mode: 'production',
  entry: './src/index.ts',
  target:'node',
  externals: [nodeExternals(),{ knex: 'commonjs knex' }],
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  resolve: {
    extensions: [ ".mjs",'.js', '.ts','.(graphql|gql)'],
    modules: [
        
        'src',
    ]
},
  module:{
      rules:[
        {
            test: /\.(graphql|gql)$/,
            exclude: /node_modules/,
            loader: 'graphql-tag/loader'
        },
        {
            test: /\.ts$/,
            exclude: /node_modules/,
            loaders: 'awesome-typescript-loader'
        }
      ]
  },
  plugins:[
    new CheckerPlugin(),
  ]
  
};

Hello, World example

add the following script to the package.json file:

"scripts":{
     "dev": "concurrently \" nodemon ./dist/bundle.js \" \" webpack --watch\" "
}

index.ts

import express, { Application } from 'express';
import {  ApolloServer , Config } from 'apollo-server-express';


const app: Application  = express();

const schema = `
    type User{
        name: String
    }
    type Query {
        user:User
    }
`
const config : Config = {
    typeDefs:schema,
    resolvers : {
        Query:{
            user:(parent,args,ctx)=>{
                return { name:"WOnder"}
            }
        }
    },
    introspection: true,//these lines are required to use the gui 
    playground: true,//   of playground

}

const server : ApolloServer = new ApolloServer(config);

server.applyMiddleware({
    app,
    path: '/graphql'
  });

app.listen(3000,()=>{
    console.log("We are running on http://localhost:3000/graphql")
})

Server config

we will use, Executable schema from Graphql-tools. It allows us to generate GraphQLSchema and allow us to join the types or resolvers from a large number of files.

src/index.ts

...
const config : Config = {
    schema:schema,// schema definition from schema/index.ts
    introspection: true,//these lines are required to use  
    playground: true,//     playground

}

const server : ApolloServer = new ApolloServer(config);

server.applyMiddleware({
    app,
    path: '/graphql'
  });
...

schema/index.ts

import { makeExecutableSchema} from 'graphql-tools';
import schema from './graphql/schema.gql';
import {user,pet} from './resolvers';

const resolvers=[user,pet];

export default makeExecutableSchema({typeDefs:schema, resolvers: resolvers as any});

Database

Let’s see the database diagram including a registry of users and their pets.

GraphQL

Migration file

for the creation of a database in Postgres we use migration files of knew

require('ts-node/register');

module.exports = {
  development:{
    client: 'pg',
    connection: {
        database: "my_db",
        user: "username",
        password: "password"
      },
    pool: {
      min: 2,
      max: 10
    },
    migrations: {
      tableName: 'knex_migrations',
      directory: 'migrations'
    },
    timezone: 'UTC'
  },
  testing:{
    client: 'pg',
    connection: {
        database: "my_db",
        user: "username",
        password: "password"
      },
    pool: {
      min: 2,
      max: 10
    },
    migrations: {
      tableName: 'knex_migrations',
      directory: 'migrations'
    },
    timezone: 'UTC'
  },
  production:{
    client: 'pg',
    connection: {
        database: "my_db",
        user: "username",
        password: "password"
      },
    pool: {
      min: 2,
      max: 10
    },
    migrations: {
      tableName: 'knex_migrations',
      directory: 'migrations'
    },
    timezone: 'UTC'
  }
};

a first migration running the file will be created:

npx knex --knexfile ./src/database/knexfile.ts migrate:make -x ts initial

and the migration file seems like this

import * as Knex from "knex";


export async function up(knex: Knex): Promise<any> {
    return knex.schema.createTable('users',(table:Knex.CreateTableBuilder)=>{
        table.increments('id');
        table.string('full_name',36);
        table.integer('country_code');
        table.timestamps(true,true);

    })
    .createTable('pets',(table:Knex.CreateTableBuilder)=>{
        table.increments('id');
        table.string('name');
        table.integer('owner_id').references("users.id").onDelete("CASCADE");
        table.string('specie');
        table.timestamps(true,true);
    })
}


export async function down(knex: Knex): Promise<any> {
}

press run for migration file

npx knex --knexfile ./src/database/knexfile.ts migrate:latest

now there are two tables in the database and we need models for each table to execute queries, src/database/models:

import {Model} from 'objection';
import {Species,Maybe} from '../../__generated__/generated-types';

import User from './User';

class Pet extends Model{
    static tableName = "pets";
    id! : number;
    name?: Maybe<string>;
    specie?: Maybe<Species>; 
    created_at?:string;
    owner_id!:number;
    owner?:User;

    static jsonSchema ={
        type:'object',
        required:['name'],

        properties:{
            id:{type:'integer'},
            name:{type:'string', min:1, max:255},
            specie:{type:'string',min:1, max:255},
            created_at:{type:'string',min:1, max:255}
        }
    };

    static relationMappings=()=>({
        owner:{
            relation:Model.BelongsToOneRelation,
            modelClass:User,
            join: {
                from: 'pets.owner_id',
                to: 'users.id',
              }
        }
    });

    
};

export default Pet;

 

import {Model} from 'objection';
import {Maybe} from '../../__generated__/generated-types';
import Pet from './Pet';



class User extends Model{
    static tableName = "users";
    id! : number;
    full_name!: Maybe<string>;
    country_code! : Maybe<string>;
    created_at?:string;
    pets?:Pet[];

    static jsonSchema = {
        type:'object',
        required:['full_name'],

        properties:{
            id: { type:'integer'},
            full_name:{type :'string', min:1, max :255},
            country_code:{type :'string', min:1, max :255},
            created_at:{type :'string', min:1, max :255}
        }
    }

    static relationMappings =()=>({
        pets: {
            relation: Model.HasManyRelation,
           modelClass: Pet,
            join: {
              from: 'users.id',
              to: 'pets.owner_id'
            }
          }
    })
}

export default User;

now we instantiate Knex and provide the instance to Objection

import dbconfig from './database/config';
const db = Knex(dbconfig["development"]);

Model.knex(db);

SCHEMA

enum Species{
    BIRDS,
    FISH,
    MAMMALS,
    REPTILES
}

type User {
    id: Int!
    full_name: String
    country_code: String
    created_at:String
    pets:[Pet]
}

type Pet {
    id: Int!
    name: String
    owner_id: Int!
    specie: Species
    created_at:String
    owner:User
}

input createUserInput{
    full_name: String!
    country_code: String!
}

input createPetInput{
    name: String!
    owner_id: Int!
    specie: Species!
}

input updateUserInput{
    id:Int!
    full_name: String
    country_code: String
}


input updatePetInput{
    id:Int!
    name: String!
}

type Query{
    pets:[Pet]
    users:[User]
    user(id:Int!):User
    pet(id:Int!):Pet
}

type Mutation{
    createPet(pet:createPetInput!):Pet
    createUser(user:createUserInput!):User
    deletePet(id:Int!):String
    deleteUser(id:Int!):String
    updatePet(pet:updatePetInput!):Pet
    updateUser(user:updateUserInput!):User
}

generating types

below packages are required for better type safety the resolvers :

 yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/
typescript-resolvers @graphql-codegen/typescript-operations 

create the config file for generating types :

/codegen.yml
overwrite: true
schema: "http://localhost:3000/graphql"
documents: null
generates:
  src/__generated__/generated-types.ts:
    config:
      mappers:
        User:'./src/database/User.ts'
        UpdateUserInput:'./src/database/User.ts'
        Pet:'./src/database/Pet.ts'
    plugins:
      - "typescript"
      - "typescript-resolvers"

add below script to packages.json :

...
"generate:types": "graphql-codegen --config codegen.yml"
...

when the server is up, then run :

yarn run generate:types

for the generation of types from Graphql read from here, it is highly suggested

resolvers

schema/resolvers/

import {Pet,User} from '../../database/models';
import {Resolvers} from '../../__generated__/generated-types';
import {UserInputError} from 'apollo-server-express';


const resolvers : Resolvers = {
    Query:{
        pet:async (parent,args,ctx)=>{
            const pet:Pet= await Pet.query().findById(args.id);

             return pet;          
        },
        pets: async (parent,args,ctx)=>{
            const pets:Pet[]= await Pet.query();

            return pets;

        }
    },
    Pet:{
        owner:async(parent,args,ctx)=>{
            const owner : User = await Pet.relatedQuery("owner").for(parent.id).first();

            return owner;
        }
    },
    Mutation:{
        createPet:async (parent,args,ctx)=>{
            let pet: Pet;
            try {
                 pet  = await Pet.query().insert({...args.pet});
               
            } catch (error) {
                throw new UserInputError("Bad user input fields required",{
                    invalidArgs: Object.keys(args),
                  });
                
            }
            return pet;
        },
        updatePet:async (parent,{pet:{id,...data}},ctx)=>{
            const pet : Pet = await Pet.query()
                                    .patchAndFetchById(id,data);

            return pet;
        },
        deletePet:async (parent,args,ctx)=>{
            const pet = await Pet.query().deleteById(args.id);
            return "Successfully deleted"
        },
    }
}


export default resolvers;
import { Resolvers} from '../../__generated__/generated-types';
import {User,Pet} from '../../database/models';
import {UserInputError} from 'apollo-server-express';

interface assertion {
    [key: string]:string | number ;
}

type StringIndexed<T> = T & assertion;

const resolvers : Resolvers ={
    Query:{
        users: async (parent,args,ctx)=>{
            const users : User[] = await User.query();
            return users;
        },
        user:async (parent,args,ctx)=>{
            const user :User = await await User.query().findById(args.id);

           return user;
        },
    },
    User:{
        pets:async (parent,args,ctx)=>{
            const pets : Pet[] = await User.relatedQuery("pets").for(parent.id);

            return pets;
        }
        
    },
    Mutation:{
        createUser:async (parent,args,ctx)=>{
            let user : User;
            try {
                user = await User.query().insert({...args.user});
            } catch (error) {
                console.log(error);
               throw new UserInputError('Email Invalido', {
                   invalidArgs: Object.keys(args),
                 });
            }
            return user;
        },
        updateUser:async (parent,{user:{id,...data}},ctx)=>{

            let user : User = await User.query().patchAndFetchById(id,data);

            return user;

        },
        deleteUser:async (parent,args,ctx)=>{
            const deleted = await User.query().deleteById(args.id);
            return "Succesfull deleted";
        },

    }
}


export default resolvers;

this will help to execute all the operations defined before

BONUS

two errors can be seen

It’s not bad to have errors, I prefer not to have errors, after this the first error is resolved by splitting knexfile.ts then put the required configuration for Knex in a separate file.

const default_config = {
    client: 'pg',
    connection: {
        database: "db",
        user: "user",
        password: "password"
      },
    pool: {
      min: 2,
      max: 10
    },
    migrations: {
      tableName: 'knex_migrations',
      directory: 'migrations'
    },
    timezone: 'UTC'
  }
  interface KnexConfig {
    [key: string]: object;
  };
  const config : KnexConfig = {
    development:{
      ...default_config
    },
    testing:{
      ...default_config
    },
    production:{
      ...default_config
    }
  };

  export default config;
require('ts-node/register');
import config from './config';


module.exports= config["development"]

the second got resolved from importing from the schema and taking help from this useful post. now we should have to work on our own Graphql API

CONCLUSION

Exciting news! Our GraphQL API is up and running. In this tutorial, we’ve covered generating types for Typescript from GraphQL and troubleshooting common issues. I trust you found this guide helpful. Stay tuned for more upcoming posts. Feel free to share your suggestions in the comment box. Thank you for tuning in, remote typescript developers!

 

Visited 1 times, 1 visit(s) today

Leave a comment

Your email address will not be published. Required fields are marked *