DEV Community

Cover image for GraphQL + TypeScript + PostgreSQL API
Wonder2210
Wonder2210

Posted on

36 8

GraphQL + TypeScript + PostgreSQL API

Introduction

In the current year, one of the most popular stacks is GraphQl and Typescript (and for a while, I think). I started recently a new project, using this stack, I did some GraphQL API's using good Vanilla Javascript before, but even I'm using Typescript for few times. I've never used it for this purpose but I didn't find a tutorial who fits with my requirements, I get it done, then I ask to my self. Why not do a guide?. Here we go

before we are starting :

Why GraphQL ?:

GraphQL provides a complete description of the data in your API, giving clients the power to ask for exactly what they need and nothing more when you have to deal with a great amount of data this is a very nice choice, you can have all the data required with just Running one Query.

Why typescript? :

Typescript is a superset of Javascript that compiles to plain JavaScript.
As JavaScript code grows it gets messier to maintain and reuse, and don't have strong type checking and compile-time error checks, that's where Typescript comes in

Why PostgreSQL?

PostgreSQL is a personal preference, is widely used, open-source, and has a great community, but I'm not going to go deep about it, You can read more about why use it here

Prerequisites

  • yarn you can use NPM no matter
  • node: v.10 or superior
  • postgresql = 12
  • some typescript knowledge

1) Folder Structure

This is how's going to be structured the project

   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       
Enter fullscreen mode Exit fullscreen mode

2) Main Dependencies

  • Apollo server: Apollo Server is a community-maintained open-source GraphQL server. It works with pretty much all Node.js HTTP server frameworks

  • Objection: i used to use sequelize but i really like objection.js because it is an ORM that embraces SQL

    Development

  • Webpack : webpack is used to compile JavaScript modules, node.js by its own doesn't accept files .gql or .graphql , that's where webpack comes in

First, we are going to install

yarn add graphql apollo-server-express express body-parser objection pg knex
Enter fullscreen mode Exit fullscreen mode

and some dev dependencies

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

3) Configurations

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"]
}
view raw tsconfig.json hosted with ❤ by GitHub

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(),
]
};

4) Hello World

Add the next scripts to your package.json


Enter fullscreen mode Exit fullscreen mode


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


Enter fullscreen mode Exit fullscreen mode

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")
})
view raw index.ts hosted with ❤ by GitHub

5) Server config

For this project we are gone to use , Executable schema from graphql-tools wich allow us to generate a GraphQLSchema instance from GraphQL schema language beside this you can also combine types and resolvers from multiple files

src/index.ts


Enter fullscreen mode Exit fullscreen mode


typescript
...
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'
});
...


Enter fullscreen mode Exit fullscreen mode

schema/index.ts


Enter fullscreen mode Exit fullscreen mode


typescript
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});


Enter fullscreen mode Exit fullscreen mode

6) Database

Now we will be working based in the next database diagram, It will be just a register of Users and their pets.
capture
Migration file
For creating the database in Postgres , we'll be using the migrations of knex

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'
}
};
view raw knexfile.ts hosted with ❤ by GitHub



and generate the first migration running:


Enter fullscreen mode Exit fullscreen mode


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


Enter fullscreen mode Exit fullscreen mode

And your migration file should look's 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> {
}
view raw migration.ts hosted with ❤ by GitHub



Then run the migration


Enter fullscreen mode Exit fullscreen mode


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


Enter fullscreen mode Exit fullscreen mode

Now we have two tables then we need the models for each table to start executing 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;
view raw Pet.ts hosted with ❤ by GitHub
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;
view raw User.ts hosted with ❤ by GitHub



then we need to instantiate Knex and give the instance to Objection


Enter fullscreen mode Exit fullscreen mode


typescript

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

Model.knex(db);


Enter fullscreen mode Exit fullscreen mode

7) 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
}
view raw schema.gql hosted with ❤ by GitHub

8) generating types

we need the following packages for better type safeting the resolvers :


Enter fullscreen mode Exit fullscreen mode


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


Enter fullscreen mode Exit fullscreen mode

create the config file for generate types :

/codegen.yml
Enter fullscreen mode Exit fullscreen mode

Enter fullscreen mode Exit fullscreen mode


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"


Enter fullscreen mode Exit fullscreen mode

add the follow script to packages.json :


Enter fullscreen mode Exit fullscreen mode


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


Enter fullscreen mode Exit fullscreen mode

once your server is up , then run :


Enter fullscreen mode Exit fullscreen mode


bash
yarn run generate:types


Enter fullscreen mode Exit fullscreen mode

if you want to go deep generating types from graphql you can read more about here, I highly suggest to do

9) 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;
view raw pet.ts hosted with ❤ by GitHub
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;
view raw user.ts hosted with ❤ by GitHub



Now you should be able to execute all the operations defined before

BONUS:

you can see two errors from typescript

errors

it's not terrible at all, but I would prefer to don't have them

then

the first one I get it solved splitting knexfile.ts and put the configuration that is required for knex in a standalone 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;
view raw config.ts hosted with ❤ by GitHub
require('ts-node/register');
import config from './config';
module.exports= config["development"]
view raw knexfile.ts hosted with ❤ by GitHub

And the second one, from the import of the schema , I get it solved with this useful post
and finally, you should have working your own graphql api

Conclusion

Congratulations ! Now you have a GraphQL API
if you get stucked at any of the steps here is the repo on github , In this tutorial we learned about, how to generate types for Typescript from graphql , solve some issues , I hope you enjoyed this post , if is it the case, please follow me here on DEV and also on twitter I'll be posting more often soon , if you have any suggestion for me I would love to know it , leave it below in the comments box ,Thanks!

AWS GenAI LIVE image

How is generative AI increasing efficiency?

Join AWS GenAI LIVE! to find out how gen AI is reshaping productivity, streamlining processes, and driving innovation.

Learn more

Top comments (1)

Collapse
 
hummingbird24 profile image

Great write up!! Thank you for taking the time to create this

Postgres on Neon - Get the Free Plan

No credit card required. The database you love, on a serverless platform designed to help you build faster.

Get Postgres on Neon

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay