GraphQL Backend - Schema-first vs Code-first
GraphQL is a query language for API created by Facebook in 2012 and open-sourced in 2015. It provides a set of features that allows building Back-end and a very optimized way for Data fetching. According to https://graphql.org
GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.
To do this, GraphQL is centered around an element called The GraphQL Schema which is the Contract between the Back-end service and the Front-end on how to consume the service and what can be served by the service.
As you will have understood, everything goes through this GraphQL schema. 😉 Now this being said, let's see what are the different ways to build it
As a backend developer, I sometimes had to wonder which approach to adopt when I had to build a GraphQL Backend. When I started working on GraphQL, I used to work with the standard way to do this task, what is called the "Schema-first" approach where the GraphQL Schema defining type, fields, resolvers function, and relationships between data are defined first to say which fields can be exposed by the service and which functions resolvers can be called to ask for those fields and which parameters are required by each resolver.
On the other hand, this approach is not the only way to build a GraphQL service since the "Code-first" approach can be another way to build a GraphQL API. In this way, you only need to write the resolvers function needed to serve a specific need with some annotations, and a build tool will compile the schema and the SDL based on types and provided annotations. We will dive deeper into it later. let's jump into each approach now.
1. Schema-first
This way of building GraphQL services has been the only way to build a GraphQL backend at the beginning and it is based on a simple concept, the "Contract" ☝️
The "Contract" here represents an agreement between the Front-end and the Back-end on how Data are defined (field names, resolvers functions, available fields, etc ..) and how all the Frontend can consume those Data. This way, Front-end developers can work asynchronously when the GraphQL schema is ready. To do so, we use the SDL (Schema Définition Language) to define the schema and then create all the resolver functions that execute and return data at runtime. Therefore, the GraphQL schema is like a Blueprint of a GraphQL API.
This pattern is used by many popular GraphQL servers. let's see this with a real example. To see this in the real world, let's build a simple service using Apollo Server and Typescript. Let's follow the same approach as shown on their website
Step 1: Create a new project
Open your favorite terminal and create a folder
mkdir demo-schema-first
cd demo-schema-first
then run the following commands yarn init --yes
to initialize a node.js project using Yarn as a package manager. this will create a package.json
Step 2: Install dependencies
yarn add @apollo/server graphql
to install useful packagesyarn add -D typescript nodemon @types/node
to install typescript and corresponding types- Now run
touch tsconfig.json
to create atsconfig.json
and paste the following code into
Add the following in your pakage.json
"type": "module",
"scripts": {
"compile": "tsc",
"start": "npm run compile && nodemon ./dist/index.js"
}
Our package.json will look like this
{
"name": "demo-schema-first",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"type": "module",
"scripts": {
"compile": "tsc",
"start": "npm run compile && node ./dist/index.js"
},
"dependencies": {
"@apollo/server": "^4.3.0",
"graphql": "^16.6.0"
},
"devDependencies": {
"@types/node": "^18.11.18",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
}
}
Create a src
folder with
mkdir src
touch src/index.ts
and paste the following to bootstrap a basic Apollo Server
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
const typeDefs = `
type Book {
id:ID!
title: String
author: String
}
type Query {
books: [Book]
}
`;
const books = [
{
id:1,
title: "The Awakening",
author: "Kate Chopin",
},
{
id:2,
title: "City of Glass",
author: "Paul Auster",
},
];
const resolvers = {
Query: {
books: () => books,
},
};
const server = new ApolloServer({
typeDefs, // IMPORTANT
resolvers, // IMPORTANT
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`🚀 Server ready at: ${url}`);
On the terminal, run the command yarn start
and you will have the following output when you open http:localhost:4000
and execute the query to get all books
In this example, there is something very important to mention. The way the Apollo Server is created. We first define the schema using SDL syntax, define the corresponding resolvers, and pass both to the ApolloServer constructor.
However, the downside to a schema-first approach is that the resolver functions must match the schema exactly. Since developers need to write the schema and resolvers separately, this becomes a challenge for very large APIs with thousands of lines.
To this day, this approach does work, but it requires a lot of workarounds and tools to help keep the code clean and inconsistencies to a minimum.
2. Code-first
As described previously, the code-first approach focuses on writing resolvers functions and annotations, then a build tool will compile the schema and the SDL based on types and provided annotations and generate a GraphQL schema based on resolvers types. This approach can speed up the development process since there is no need to check each time that the resolvers match any schema.
To work with the code-first approach, you will need some third-party libraries like tpegraphql or Nexus. Let's build a GraphQL API from our previous example using TypeGraphQL
1. Create a new project
Open your favorite terminal and create a folder with
mkdir demo-code-first
cd demo-code-first
then run the following commands
yarn init --yes
to initialize a node.js project using Yarn as a package manager. this will create apackage.json
and add the following
"scripts": {
"compile": "tsc",
"start": "npm run compile && nodemon ./src/index.ts"
}
2. Install dependencies
yarn add -D typescript ts-node nodemon @types/node
to install typescript and corresponding types-
yarn add @apollo/server typedi class-validator reflect-metadata graphql@15.8.0 type-graphql@2.0.0-beta.1
which are dependencies required for our use case.
Note: Since we are working with Apollo server v4, we use type-graphql
version 2.0.0-beta.1
to avoid errors since Apollo Server V4.0 has "Dropped support for versions of the GraphQL library prior to v16.6.0". You can have a look at the issue here 👉 on GitHub for more details
Our package.json look like this
{
"name": "demo-code-first",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"compile": "tsc",
"start": "yarn compile && nodemon ./src/index.ts"
},
"dependencies": {
"@apollo/server": "^4.3.0",
"class-validator": "^0.14.0",
"graphql": "^16.6.0",
"reflect-metadata": "^0.1.13",
"type-graphql": "2.0.0-beta.1",
"typedi": "^0.10.0"
},
"devDependencies": {
"@types/node": "^18.11.18",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
}
}
Create a src
folder with
mkdir src
touch src/index.ts
and paste the following to bootstrap a basic Apollo Server
We must ensure that it is imported at the top of our entry file (before we use/import type-graphql
or our resolvers):
import "reflect-metadata";
3.TypeScript configuration
It's important to set these options in the tsconfig.json
file of our project:
{
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
TypeGraphQL
is designed to work with Node.js LTS (10.3+, 12+) and the latest stable releases. It uses features from ES2018 so we should set our tsconfig.json
file appropriately:
{
"target": "es2018" // or newer if your node.js version supports this
}
Due to using the graphql-subscription
dependency that relies on an AsyncIterator
, we may also have to provide the esnext.asynciterable
to the lib
option:
{
"lib": ["es2018", "esnext.asynciterable"]
}
All in all, the minimal tsconfig.json
file example looks like this:
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"outDir": "dist",
"esModuleInterop": true,
"lib": ["es2018", "esnext.asynciterable"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
4.TypeScript configuration
It's important to set these options in the tsconfig.json
file of our project:
{
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
TypeGraphQL
is designed to work with Node.js LTS (10.3+, 12+) and the latest stable releases. It uses features from ES2018 so we should set our tsconfig.json
file appropriately:
{
"target": "es2018" // or newer if your node.js version supports this
}
Due to using the graphql-subscription
dependency that relies on an AsyncIterator
, we may also have to provide the esnext.asynciterable
to the lib
option:
{
"lib": ["es2018", "esnext.asynciterable"]
}
All in all, the minimal tsconfig.json
file example looks like this:
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"lib": ["es2018", "esnext.asynciterable"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Let's create our server. First, create the following files inside src
folder
book.ts
import { Field, ID, ObjectType } from "type-graphql";
@ObjectType()
export class Book {
@Field((type) => ID)
id: number;
@Field()
title: string;
@Field()
author: string;
constructor(id: number, title: string, author: string) {
this.title = title;
this.author = author;
this.id = id;
}
}
bookResolver.ts
import { Arg, Query, Resolver } from "type-graphql";
import { Book } from "./book";
import { BookService } from "./bookService";
import { Container } from "typedi";
@Resolver((of) => Book)
export class BookResolver {
constructor(private bookService: BookService) {}
@Query((returns) => [Book])
async books() {
return Container.get(BookService).getBooks();
}
@Query((returns) => Book)
async book(@Arg("id") id: number) {
return Container.get(BookService).getBook(id);
}
}
bookService.ts
thas will behave like our layer between resolvers and Database where we fetch data
import { Service } from "typedi";
import { Book } from "./book";
@Service()
export class BookService {
books = [
{
id: 1,
title: "The Awakening",
author: "Kate Chopin",
},
{
id: 2,
title: "City of Glass",
author: "Paul Auster",
},
];
getBooks = (): Book[] => {
return this.books;
};
getBook = (id: number): Book => {
return this.books.find((book) => book.id === id);
};
}
index.ts
import "reflect-metadata";
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
import { buildSchema } from "type-graphql";
import { BookResolver } from "./bookResolver";
import * as path from "path";
const bootstrap = async () => {
const schema = await buildSchema({
resolvers: [BookResolver],
emitSchemaFile: path.resolve(__dirname, "schema.gql"),
});
const server = new ApolloServer({ schema });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`🚀 Server ready at: ${url}`);
};
bootstrap();
On the terminal, run the command yarn start
and you will have the following output when you open http:localhost:4000
and execute the query to get all books
This will output the exact same result as the previous approach using schema first.
Note. Something important to mention is this line on our index.ts
file
const schema = await buildSchema({
resolvers: [BookResolver],
emitSchemaFile: path.resolve(__dirname, "schema.gql"), /// THIS LINE
});
With this option, we tell our server to output the schema file at the root of our src
folder. If we take a look at the root folder, we will see a schema.gql
file which contains the blueprint of our Graphql API.
3. Conclusion
There are always pros and cons to everything. In my opinion, you should choose the one that is good for you and which you are comfortable with. I can also say that the schema-first approach is good for people who just started working with graphql since you will understand if it is the ideal way for you to develop Apis and is the most popular way on the market. However, if you find that over time, it becomes difficult to keep up with the complexity of the project, you can learn Code-first.
Thanks for reading 😉
Follow me on social media to stay up to date with latest posts.