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:
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
- yarn NPM can be used
- node: v.10 or superior
- PostgreSQL = 12
- 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.
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!