This article is the first part of building a production grade nodejs,graphql and typescript server with auto deployment pipeline.
there are tons of articles and tutorials are available to explain how to build a scalable backend solutions. but, what i felt missing was a solution that connects all the dots with a simple and proper use-cases.
Most of the time, we won't directly jump into building the application by coding it. we would start with designing how the backend should be based on the application requirement. knowing how to do that is an important skill to have than just knowing the syntax of x or y framework.
Before coding, let's see what we are going to build to learn the concepts. we will be building a Image management application where users can manage their image resources.
Mainly, it will contain login,signup and main application. In main application, user can upload images and view all the uploaded images. Even though it's a simple use-case, we can understand all the concepts using this simple use-case.
Signup Screen
Login Screen
Main App Screen
Checkout the Figma file Here
Now, we know what application we are going to build. let's apply all the best practices to build a Nodejs,GraphQL and TypeScript Server.
This part is going to explain how to setup the application and create a query and mutations using GraphQL and TypeScript.
we will be using type-graphql for your application. let's install the required dependancies for our application,
1npm i apollo-server-express express graphql reflect-metadata type-graphql mongoose
1npm i --save-dev @types/express @types/graphql @types/node nodemon ts-node ts-node-dev typescript
Above dependancies helps us to manage the types and development server daemon for our application.
Now, we need to configure tsconfig file in our application. create a file tsconfig.json
and add the following,
1{2 "compilerOptions": {3 "target": "es6",4 "module": "commonjs",5 "lib": ["dom", "es6", "es2017", "esnext.asynciterable"],6 "sourceMap": true,7 "outDir": "./dist",8 "moduleResolution": "node",9 "declaration": false,10 "composite": false,11 "removeComments": true,12 "noImplicitAny": true,13 "strictNullChecks": true,14 "strictFunctionTypes": true,15 "noImplicitThis": true,16 "noUnusedLocals": true,17 "noUnusedParameters": true,18 "noImplicitReturns": true,19 "noFallthroughCasesInSwitch": true,20 "allowSyntheticDefaultImports": false,21 "emitDecoratorMetadata": true,22 "experimentalDecorators": true,23 "skipLibCheck": true,24 "baseUrl": ".",25 "rootDir": "src"26 },27 "exclude": ["node_modules"],28 "include": ["./src/**/*.tsx", "./src/**/*.ts"]29}
create a file src/server.ts and add the following code,
1import { ApolloServer } from "apollo-server-express"2import * as Express from "express"3import "reflect-metadata"4import { buildSchema } from "type-graphql"56import * as Mongoose from "mongoose"7async function startServer() {8 require("dotenv").config(__dirname + ".env")910 const schema = await buildSchema({11 resolvers: [],12 emitSchemaFile: true,13 })1415 const app = Express()1617 const MONGO_USER = process.env.MONGODB_USER18 const MONGO_PASS = process.env.MONGODB_PASS1920 Mongoose.connect(21 `mongodb+srv://${MONGO_USER}:${MONGO_PASS}@cluster0-xv4mh.mongodb.net/test?retryWrites=true&w=majority`,22 {23 useNewUrlParser: true,24 useUnifiedTopology: true,25 }26 )27 .then(res => {28 console.log("Mongodb is connected successfully")2930 const server = new ApolloServer({31 schema,32 context: () => ({}),33 })3435 server.applyMiddleware({ app })36 const PORT = process.env.PORT37 app.listen(PORT, () => {38 console.log(`server is running on PORT ${PORT}`)39 })40 })41 .catch(err => {42 console.log(err)43 })44}4546startServer()
On the above code, we create a async function to bootstrap our application.
1async function startServer() {2 //Main code comes Here3}45startServer()
Inside the function, we build the schema for your application by providing the resolvers. yeah, you read it right.. Graphql schemas are auto generated in type-graphql.
1const schema = await buildSchema({2 resolvers: [//our resolvers come here],3 emitSchemaFile: true,4 });
After that, we connect with MongoDB Database and start our serve.
1Mongoose.connect(2 `mongodb+srv://${MONGO_USER}:${MONGO_PASS}@cluster0-xv4mh.mongodb.net/test?retryWrites=true&w=majority`,3 {4 useNewUrlParser: true,5 useUnifiedTopology: true,6 }7)8 .then(res => {9 console.log("Mongodb is connected successfully")1011 const server = new ApolloServer({12 schema,13 context: () => ({}),14 })1516 server.applyMiddleware({ app })17 const PORT = process.env.PORT18 app.listen(PORT, () => {19 console.log(`server is running on PORT ${PORT}`)20 })21 })22 .catch(err => {23 console.log(err)24 })
Environment Variable Setup
we will be using dotenv in our application to manage the environment variables for development.
1require("dotenv").config(__dirname + ".env")
.env
1PORT=56782MONGODB_USER=root3MONGODB_PASS=Root!23456
Now, we got everything to run the basic server in typescript. To run the development typescript server, add the following script in application.
1"dev": "ts-node-dev --respawn src/server.ts"
you can run your development server using the command,
1npm run dev
Let's break our application into domains. so, that we can wrap our business logic based on the domains.
Here, we can consider the entities as our feature. we have User and Asset as an entity. Let's create the file structure based on that,
Let's start with User Service and create all the Queries and Mutations for that service.
To work with MongoDB and Typescript, let's install few dependancies that are required.
1npm i --save-dev @types/mongoose
After that, create a UserModel.ts file in the UserService and add the following code.
1import * as Mongoose from "mongoose"23export interface IUser extends Mongoose.Document {4 name: String5 email: String6 password: String7}89const UserSchema: Mongoose.Schema = new Mongoose.Schema(10 {11 name: {12 type: String,13 required: true,14 },15 email: {16 type: String,17 required: true,18 },19 password: {20 type: String,21 required: true,22 },23 },24 { timestamps: true }25)2627export default Mongoose.model<IUser>("User", UserSchema)
On the above code, we define a mongoose schema which returns type mongoose schema along with the fields for User Model.
After that, we define an interface for our User Model,
1export interface IUser extends Mongoose.Document {2 name: String3 email: String4 password: String5}
If you are new to typescript interfaces, checkout this docs to know more.
1export default Mongoose.model<IUser>("User", UserSchema)
Finally, we export the mongoose model which implements IUser interface. we have successfully create our mongoose model.
Note: Here i am using MongoDB as database. if you want to use SQL, checkout this series from ben awad
Every resolver in type-graphql starts with an Resolver decorator tag with it.
1@Resolver(of => UserSchema)2export class UserResolver {3 //Queries and Mutation Comes Here4}
Here, we define it is a resolver of a particular Schema. Let's write the schema file for user and continue with resolver function.
UserSchema.ts
1import { Field, ObjectType, ID } from "type-graphql"2import { IsEmail, Length } from "class-validator"34@ObjectType({ description: "User Schema" })5export default class User {6 @Field(() => ID)7 id: String89 @Field()10 @Length(1, 30)11 name: String1213 @Field()14 @IsEmail()15 @Length(1, 30)16 email: String1718 password: String19}
On the above code, we define it is an Object type with a decorator. For every field, we have a Field decorator. So that, it will reflects in the graphql schema. we can also validate different parameters such as length and email.
If you watch it carefully, we haven't set any decorators for password. if you don't want a value to be in the graphql schema. we have left it out from Field decorator. import the schema in our resolver.
1import UserSchema from "./UserSchema"
Let's write a sample Query and test it out whether our setup is working correctly.
1@Query(() => String)2 sample(): String {3 return "Hello";4 }
Every Query and Mutation starts with Query or Mutation Decorator and it's return type. Here, we have a return type of String.
UserResolver.ts
1import {2 Arg,3 FieldResolver,4 Query,5 Mutation,6 Resolver,7 Ctx,8 Root,9} from "type-graphql"10import UserSchema from "./UserSchema"1112@Resolver(of => UserSchema)13export class UserResolver {14 @Query(() => String)15 sample(): String {16 return "Hello"17 }18}
After that, import the resolver in our server file and define it in our schema resolvers
1import { UserResolver } from "./UserService/UserResolver"23const schema = await buildSchema({4 resolvers: [UserResolver],5 emitSchemaFile: true,6})
Now, we have a Typescript GraphQL Server running successfully in our machine.
I am wrapping up this part here, Let's see how to create
functionalities in upcoming articles. Until then Happy coding
No spam, ever. Unsubscribe anytime.