Hello again !!! This is the third part of this extensive tutorial and I would like to discuss a few important parts of API development. As I am developing a dummy food app using the IOC principle, I have already added a few modules like the restaurant, item, cart, order, discount, etc and it is available to this repo Food-App . And customers will also be going to use this application so I have decided, Why don’t we implement GraphQL to consume those APIs? So let’s add GraphQL to our existing project. In case, if you want to check previous tutorials — Stage: 1.0.0 and Stage 1.0.1

N.B: Prior knowledge of express, typescript, Postgres & a little bit of docker, docker-compose, and sound knowledge of GraphQL are required.

What is GraphQL & Why do we need that?
Well, GraphQL is a querying language which means that you can query your backend/API based on your needs. More extensively, let's say you are getting this response from an API

And from this response you only need the serial_number then wouldn’t be great if you get what you really need to have? And that's why GraphQL comes into this place. Maybe you are thinking this response is not a big deal which is true but just think about a huge response and in the low network it can slow down the app’s response time.

What is an Apollo server?
Obviously, it’s a server and it’s a spec-compliant for GraphQL server. This means that you can manage/configure the GraphQL server with ease. This is a diagram from Apollo server documentation-

N.B. For now we will going to use Apollo Server V3

Let’s discuss in detail using this project’s few APIs. We will be going to call our existing REST API using the new GraphQL API. And those features are-

  1. Add/Update/Delete item to the cart.
  2. Create Orders by customers.
  3. List of active restaurants.

Before we move further I would like to point out 5 basic things.
Schema: This one means the shape of your data which is similar to model/entity class files. And schema is the main ingredient of your curry.

Query: Query is like the commands for the GraphQL server to get the right data. This one is only usable for GET requests like in the regular REST API.

Mutation: Yes you are right to mutate the data which is similar to POST/PUT/PATCH/DELETE request in REST API.

Resolver: You can say it’s the logical part of managing Query & Mutation. Can be known as the decision maker.

Data Source: You can guess it from its name. It is the source of data or you can say, it’s the wire to connect with the APIs. Take a look at the diagram.

In this project, we are going to use the RESTDataSource as I already told you. So let’s jump in-

Step-1: Let’s add some npm packages

npm install @graphql-tools/schema apollo-datasource-rest apollo-server-express 
express-graphql graphql graphql-constraint-directive

Our folder structure will be -

...
src
- graphql
- modules
- cart
- test
- cart.datasource.ts
- cart.schema.ts
- cart.resolver.ts
- item
- order
- restaurent
- public
- test-utils
- index.datasource.ts
- index.schema.ts
- index.resolver.ts
- apollo.server.ts
- graphql-error-format.ts
- server.ts
...

Step-2: Let’s define schema-

cart.schema.ts -

import { gql } from "apollo-server-express";

export default gql`
type Cart {
uuid: ID!
cart_amount: Float!
total_amount: Float!
rebate_amount: Float!
cart_date: String!
cart_status: String!
cart_item: [CartItem!]
}

type CartItem {
uuid: ID!
qty: Int!
amount: Float!
total_amount: Float!
item: Item!
}

input CartCreateInput {
uuid: String! @constraint(format: "uuid")
qty: Int! @constraint(min: 1)
restaurent_uuid: String! @constraint(format: "uuid")
}

input CartUpdateInput {
uuid: String! @constraint(format: "uuid")
qty: Int! @constraint(min: 1)
cart_uuid: String! @constraint(format: "uuid")
}

input CartDeleteInput {
cart_uuid: String! @constraint(format: "uuid")
item_uuid: String! @constraint(format: "uuid")
}

input CartUuid {
uuid: String! @constraint(format: "uuid")
}

extend type Query {
cartInfo(input: CartUuid!): Cart!
}

extend type Mutation {
createCart(input: CartCreateInput!): Cart
updateCart(input: CartUpdateInput!): Cart
deleteCart(input: CartDeleteInput!): Cart
}
`

Let me explain those keywords, types, etc.
type: The type keyword is the most basic component of graphql which is also represented as Object Types. Inside the object, those are the properties of the object and data-types of those properties. Those datatypes are Scalar types(Int, String, Float, ID), Object types(CartItem, Cart), Query types, Input types, and Mutation.

input: They are the input value/params for a payload.
Query: Queries for the results from the respective API. Can be compared to a regular function in the programming language.

Mutation: Can be compared to those functions which are always used to perform the CRUD operation.
And the exclamatory sign (!) means the mandatory fields.

@constraint: I am using a package for more convenient validation called graphql-constraint-directive

I am skipping other schema because the declaration is also same for them. May be you are wondering what is extend keyword. Well, you can compare with the append function in JS. Because when app gets bigger, it is better to keep everything aligned & separate for better organization of the application.
N.B. The full code is available at the repo. No worries.

cart.datasource.ts-

import { RequestOptions, RESTDataSource } from "apollo-datasource-rest";
import { CartReponse } from "../../../shared/utils/response.utils";

export class CartDataSource extends RESTDataSource {
constructor(
) {
super();
this.baseURL = process.env.DOMAIN_URL;
}

protected willSendRequest(request: RequestOptions): void | Promise<void> {
request.headers.set('authorization', this.context.access_token);
}

async createCart(restaurentUuid: string, cartObj: Object): Promise<CartReponse> {
return await this.post(`cart/restaurent/${encodeURIComponent(restaurentUuid)}`, cartObj);
}

async updateCart(cartUuid: string, cartObj: Object): Promise<CartReponse> {
return await this.post(`cart/${encodeURIComponent(cartUuid)}`, cartObj);
}

async deleteCart(cartUuid: string, itemUuid: string): Promise<CartReponse> {
return await this.delete(`cart/${encodeURIComponent(cartUuid)}/${encodeURIComponent(itemUuid)}`);
}

async getCart(cartUuid: string): Promise<CartReponse> {
return await this.get(`cart/${encodeURIComponent(cartUuid)}`);
}
}

Here —
encodeURIComponent: Standard JS function that encodes the special characters in a URI which can prevent the possible injection attack.

willSendRequest: The override method of RESTDataSource class. This is an interceptor which means you can modify the request before they’re sending to the API. As you can see here, I am sending the access token because it is an authorized request.
this.context: Skip it for now, you will understand when we will write the final apollo server class.

N.B. For more info of Data sources, visit- Apollo DataSource

cart.resolver.ts-

export default {
Query: {
cartInfo: async (_source: any, args: any, context: any) => {
const value = await context.dataSources.cartDataSource.getCart(args.uuid);
return value.result;
},
},
Mutation: {
createCart: async (_source: any, args: any, context: any) => {
const restaurentUuid = args.input.restaurent_uuid;
delete args.input.restaurent_uuid;
const value = await context.dataSources.cartDataSource.createCart(restaurentUuid, args.input);
return value.result;
},
updateCart: async (_source: any, args: any, context: any) => {
const cartUuid = args.input.cart_uuid;
delete args.input.restaurent_uuid;
const value = await context.dataSources.cartDataSource.updateCart(cartUuid, args.input);
return value.result;
},
deleteCart: async (_source: any, args: any, context: any) => {
const value = await context.dataSources.cartDataSource.deleteCart(args.input.cart_uuid, args.input.item_uuid);
return value.result;
},
}
}

This is the logical bridge between schema & datasource.

index.datasource.ts -

import { CartDataSource } from "./modules/cart/cart.datasource";
import { OrderDataSource } from "./modules/order/order.datasource";
import { PublicDataSource } from "./modules/public/public.datasource";

export {
CartDataSource,
OrderDataSource,
PublicDataSource,
}

index.resolver.ts -

import CartResolver from './modules/cart/cart.resolver';
import OrderResolver from './modules/order/order.resolver';
import RestaurentResolver from './modules/public/public.resolver';

export default [CartResolver, OrderResolver, RestaurentResolver];

index.schema.ts -

import { gql } from "apollo-server-express";
import CartSchema from "./modules/cart/cart.schema";
import OrderSchema from "./modules/order/order.schema";
import ItemSchema from "./modules/item/item.schema";
import RestaurentSchema from "./modules/restaurent/restaurent.schema";

const BaseSchema = gql`
type Query {
_:Boolean
}

type Mutation {
_:Boolean
}
`;

export default [BaseSchema, CartSchema, OrderSchema, ItemSchema, RestaurentSchema];

As you can see here I am skipping the rest of the module because their prototypes will be the same.
Now let’s create the apollo.server.ts -

import { Request, Response } from 'express';
import { makeExecutableSchema } from "@graphql-tools/schema";
import { constraintDirective, constraintDirectiveTypeDefs, createApolloQueryValidationPlugin } from 'graphql-constraint-directive';
import { ApolloServer } from "apollo-server-express";
import { CartDataSource, OrderDataSource, PublicDataSource } from "./graphql/index.datasource";
import resolvers from "./graphql/index.resolver";
import typeDefs from "./graphql/index.schema";
import { graphQlFormatError } from './graphql-error-format';


const getHttpContext = ({ req, res }: { req: Request, res: Response }) => {
return {
'access_token': req.headers?.authorization || '',
};
}

let schema = makeExecutableSchema({
typeDefs: [constraintDirectiveTypeDefs, typeDefs],
resolvers,
});

schema = constraintDirective()(schema);
const plugins = [
createApolloQueryValidationPlugin({
schema
})
];

const apolloServer = new ApolloServer({
schema,
plugins,
context: getHttpContext,
dataSources: () => {
return {
orderDataSource: new OrderDataSource(),
cartDataSource: new CartDataSource(),
publicDataSource: new PublicDataSource(),
};
},
debug: process.env.NODE_ENV === 'dev' ? true : false,
formatError: (error: any) => graphQlFormatError(error),
});

export default apolloServer;

schema: The combination of all the schema & resolver.
plugins: Using this one for directive validation.
context: You can add/modify the request objects using this method. Can be used to add external values.
dataSources: Contain all the data sources of the application.
graphQlFormaterror: You can format your error response before sending it to the client.

import { AuthenticationError, UserInputError, ValidationError } from "apollo-server-express";
import { GraphQLError } from "graphql";

export const graphQlFormatError = (error: any) => {
const errCode = error.extensions?.code;
const respErr: any = error.extensions?.response;

switch (errCode) {
case 'UNAUTHENTICATED':
return new AuthenticationError(respErr.body.message, {
'code': 'UNAUTHENTICATED',
});
case 'BAD_USER_INPUT':
return new UserInputError(error.message, {
'code': 'BAD_USER_INPUT',
});
case 'FORBIDDEN':
return new UserInputError(respErr.body.message, {
'code': 'FORBIDDEN',
});
case 'GRAPHQL_VALIDATION_FAILED':
return new ValidationError(error.message);
default:
return new GraphQLError(respErr.body.message);
}
}

N.B. For more info of ApolloServer instance — ApolloServer

Now it’s time to expose our GraphQL API. Modify the server.ts file -

...
export const server = new InversifyExpressServer(container, router);
server.setConfig(async app => {
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
await apolloServer.start();
apolloServer.applyMiddleware({ app, path: '/graphql' });
});
...

If everything is going well then run this command in the root path of this project —

docker-compose -f docker-compose.dev.yml --env-file ./env/.env.dev build --no-cache
docker-compose -f docker-compose.dev.yml --env-file ./env/.env.dev up

Now we can reach our GraphQL API like this — http://localhost:3000/graphql
you will see something like this. Now click on the Query your server button to go to the Playground.

This is a GraphQL playground hosted by the Apollo server.

Variables: This bottom center section is used for adding the payload to the request. Similar to the HTTP request body.
Headers: This is similar to the HTTP request header. Here I am adding access_token value to keep the user authenticate.

Sample of a createCart mutation-

The rest of the modules can be developed this way. If you want to look at the repo here is it — FoodApp.

So we built the application but the most important part is left which is Unit-Test. Let’s do this in the next chapter.

!!! Thank You !!!

--

--

Tushar Roy Chowdhury

I am a passionate programmer and always keep eye on new technologies and scrutinize them until getting into shape.