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
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
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
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"] | |
} |
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
json
"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") | |
}) |
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
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'
});
...
schema/index.ts
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});
6) Database
Now we will be working based in the next database diagram, It will be just a register of Users and their pets.
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' | |
} | |
}; |
and generate the first migration running:
bash
npx knex --knexfile ./src/database/knexfile.ts migrate:make -x ts initial
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> { | |
} | |
Then run the migration
bash
npx knex --knexfile ./src/database/knexfile.ts migrate:latest
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; |
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; |
then we need to instantiate Knex and give the instance to Objection
typescript
import dbconfig from './database/config';
const db = Knex(dbconfig["development"]);
Model.knex(db);
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 | |
} |
8) generating types
we need the following packages for better type safeting the resolvers :
bash
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/
typescript-resolvers @graphql-codegen/typescript-operations
create the config file for generate types :
/codegen.yml
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 the follow script to packages.json :
json
...
"generate:types": "graphql-codegen --config codegen.yml"
...
once your server is up , then run :
bash
yarn run generate:types
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; |
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; |
Now you should be able to execute all the operations defined before
BONUS:
you can see two errors from typescript
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; |
require('ts-node/register'); | |
import config from './config'; | |
module.exports= config["development"] |
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!
Top comments (1)
Great write up!! Thank you for taking the time to create this