DEV Community

Sebastian hilton
Sebastian hilton

Posted on

Building a scalable and extensive Graphql Server

Finding the perfect open source graphql server is not an easy task. When I was searching for a solution that combined the great Prisma ORM and Graphql-Yoga I wasn't able to find a workable solution anywhere. So I made my own and extended it with Typescript support, Envelop extensions, SOFA api support, and file upload support.

The first thing is what are all of these technologies that we are including in our server:

First up is, What is Graphql?

GraphQL is an open-source data query and manipulation language for APIs, and a runtime for fulfilling queries with existing data. GraphQL was developed internally by Facebook in 2012 before being publicly released in 2015.

What is Prisma?

Prisma unlocks a new level of developer experience when working with databases thanks to its intuitive data model, automated migrations, type-safety & auto-completion.

*What is Graphql-yoga?
*

GraphQL Yoga is a batteries-included cross-platform GraphQL over HTTP spec-compliant GraphQL server powered by Envelop and GraphQL Tools that runs anywhere; focused on easy setup, performance and great developer experience.

*What is Typegraphql?
*

Modern framework for GraphQL API in Node.js, used to provide typescript and graphql to Prisma.

Lets get started by creating a directory for our project:

open your command prompt or terminal:

mkdir apiserver

Enter fullscreen mode Exit fullscreen mode

then change to that directory:

cd apiserver

Enter fullscreen mode Exit fullscreen mode

_Make sure you install Nodejs to your operating system before continuing.
_
now lets create a npm project:

npm init -y
Enter fullscreen mode Exit fullscreen mode

(-y is used to quickly except all default values)

create a file in your directory called index.ts

Now to install all that is required lets install everything at once:

npm install ts-node type-graphql @prisma/client@4.6.1 prisma graphql-yoga @graphql-yoga/plugin-sofa typescript graphql @types/node @envelop/core class-validator reflect-metadata typegraphql-prisma 
Enter fullscreen mode Exit fullscreen mode

A bit of what we just installed:

  • ts-node so we can run .ts (typescript) files
  • type-graphql to provide typescript and graphql to prisma
  • typescript to add typescript to our server
  • @prisma/client and prisma to install the Prisma ORM to allow connecting to our databases. We are using version 4.6.1 because thats the latest version that typegraphql supports.

  • graphql-yoga/node is a package that is the base of our server

  • @graphql-yoga/plugin-sofa is to generate Rest APIs from our graphql schema

  • graphql to allow graphql support in our project

  • @types/node for node.js typescript support

  • @envelop/core to allow envelop plugins in our server

  • typegraphql-prisma for type-graphql and prisma integration

  • class-validator is a decorator-based property validation for classes.

  • reflect-metadata is required to make the type reflection work

now lets create a typescript configuration project within our server.

npx tsc --init
Enter fullscreen mode Exit fullscreen mode

in our root directory (the main directory of our server) and run:

npx prisma init --datasource-provider sqlite
Enter fullscreen mode Exit fullscreen mode

that initiated a Prisma directory with the schema.prisma file inside.

this schema file shows

Image description

lets update our schema.prisma file to add typegraphql support:

in your file you should already have

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}
Enter fullscreen mode Exit fullscreen mode

Lets migrate our database:

npx prisma migrate dev --name init

Enter fullscreen mode Exit fullscreen mode

Other options can be found here for Prisma supported databases and how to use them: Prisma Databases

now lets add:

generator typegraphql {
  provider           = "typegraphql-prisma"
  output             = "../prisma/generated/type-graphql"
  emitTranspiledCode = "true"
}
Enter fullscreen mode Exit fullscreen mode

What the above does is add a generator that will generate a generated folder with our CRUD functionality, resolvers, and other code to make our schema type-safe and ready for our graphql server.

more on this here: Typegraphql-Prisma

Now lets create a dev.db file in our root folder for our sqlite database.

now lets add our data to our database by running:

npx prisma db push
Enter fullscreen mode Exit fullscreen mode

Now our two models are in our dev.db file. Great we are ready to generate the data so that it is available for our server to detect.

npx prisma generate

Enter fullscreen mode Exit fullscreen mode

Now the prisma side of things are completed, we need to work on our server.

in our index.ts file lets add some things, first we need to import all of our packages that we downloaded earlier:

require("reflect-metadata");

import { buildSchema } from "type-graphql";
import * as path from "path";
import { PrismaClient } from "@prisma/client";
import { useParserCache } from '@envelop/parser-cache';
import { useValidationCache } from '@envelop/validation-cache';

import { createYoga } from 'graphql-yoga';
import { createServer } from 'node:http';
import { createFetch } from '@whatwg-node/fetch';
import { useGraphQlJit } from '@envelop/graphql-jit';
import { resolvers } from "../prisma/generated/type-graphql";
import { useSentry } from '@envelop/sentry';
import { useSofaWithSwaggerUI } from '@graphql-yoga/plugin-sofa'
import { APIGatewayEvent, APIGatewayProxyResult, Context } from 'aws-lambda'

import '@sentry/tracing';
//import { ApolloGateway } from '@apollo/gateway'
//import { useApolloFederation } from '@envelop/apollo-federation'
import fastify, { FastifyRequest, FastifyReply } from 'fastify'
Enter fullscreen mode Exit fullscreen mode

So with my above imports you can see i added reflect-metadata at the top, in order for typegraphql to work properly it must have that package at the top.

There are also a lot of other packages that are here that we didn't discuss earlier. For instance all the @envelop plugins are optional and added because of things like error reporting, logging, and running in a node environment such as Fastify and Node which you can substitute for other frameworks if you wish.

here are the supported frameworks: Graphql-yoga Integrations

and then go to integrations on the left to select your nodejs framework of choice.

Now lets add the rest of our code, the following will be:

  • fastify will be our framework of choice with logging support
  • cors module added because if added to another nodejs application, this server will be available at localhost:4000/graphql and should be accessible via a graphql-client queries and mutations.

  • We will create our main server function to hold our graphql server, prisma connection, schema, and resolvers, envelop plugins, logging functionality, cors, plugins, sofa with swagger support, and file upload functionality within the createFetch function.

Then we will call our server using the

const server = createServer(yoga) 
Enter fullscreen mode Exit fullscreen mode

We will then add our routing for our graphql server with

app.route({})
Enter fullscreen mode Exit fullscreen mode

Other personal features I added was AWS Lambda functionality for serverless deployment and commented out is support for Apollo Federation which allows multiple graphql endpoints in our app.

Apollo Federation can be commented out if you have a graphql url to add or else it will cause the server to crash as it can't detect the invalid url.

Lets add some additional functionality for our server:

npm install @envelop/graphql-jit @envelop/parser-cache @envelop/sentry @envelop/validation-cache @sentry/core @sentry/tracing @types/graphql @types/graphql-fields @whatwg-node/fetch aws-lambda fastify cors graphql-fields graphql-scalars graphql-ws tslib @types/aws-lambda
Enter fullscreen mode Exit fullscreen mode

While most of the above you might not need to build your own server, these are additional functionality to help with error reporting (Sentry), graphql support for typescript (Typegraphql), our nodejs framework (fastify), graphql-subscriptions (graphql-ws), serverless deployment (aws-lambda), and validation & caching support.

The final code is below:

require("reflect-metadata");

import { buildSchema } from "type-graphql";
import * as path from "path";
import { PrismaClient } from "@prisma/client";
import { useParserCache } from '@envelop/parser-cache';
import { useValidationCache } from '@envelop/validation-cache';

import { createYoga } from 'graphql-yoga';
import { createServer } from 'node:http';
import { createFetch } from '@whatwg-node/fetch';
import { useGraphQlJit } from '@envelop/graphql-jit';
import { resolvers } from "../prisma/generated/type-graphql";
import { useSentry } from '@envelop/sentry';
import { useSofaWithSwaggerUI } from '@graphql-yoga/plugin-sofa'
import { APIGatewayEvent, APIGatewayProxyResult, Context } from 'aws-lambda'

import '@sentry/tracing';
//import { ApolloGateway } from '@apollo/gateway'
//import { useApolloFederation } from '@envelop/apollo-federation'
import fastify, { FastifyRequest, FastifyReply } from 'fastify'

// This is the fastify instance you have created
const app = fastify({
  logger: true
})

// Setting cors and logging capabilities

var cors = require('cors')

app.options('*', cors())

// Initialize the gateway
/* const gateway = new ApolloGateway({
  serviceList: [
    { name: 'First', url: process.env.GRAPHQL_ENV },
    //{ name: 'products', url: 'http://localhost:4002' }
  ]
}) */

// Pulling our Graphql Resolvers from Type-graphql & Prisma generation

async function main() {
  const schema = await buildSchema({
    resolvers,
    emitSchemaFile: path.resolve(__dirname, "./generated-schema.graphql"),
    validate: false,
  });

  // Make sure all services are loaded
  // await gateway.load()

  // Connect to Prisma

  const prisma = new PrismaClient();
  await prisma.$connect();

  // Graphql Server main function 

  const yoga = createYoga < {
    req: FastifyRequest
    reply: FastifyReply
    event: APIGatewayEvent
    lambdaContext: Context
  } > ({
    // Integrate Fastify logger
    logging: {
      debug: (...args) => args.forEach((arg) => app.log.debug(arg)),
      info: (...args) => args.forEach((arg) => app.log.info(arg)),
      warn: (...args) => args.forEach((arg) => app.log.warn(arg)),
      error: (...args) => args.forEach((arg) => app.log.error(arg))
    },
    schema,
    //context: contextCreator,
    batching: true,
    cors: {
      origin: '*',
      credentials: true,
    },
    context: ({}) => ({
      prisma,
    }),
    plugins: [
      useParserCache({}),
      useValidationCache({}),
      useGraphQlJit({}),
      useSentry({
        includeRawResult: false, // set to `true` in order to include the execution result in the metadata collected
        includeResolverArgs: false, // set to `true` in order to include the args passed to resolvers
        includeExecuteVariables: false, // set to `true` in order to include the operation variables values
      }),
      /* useApolloFederation({
         gateway
       }) */
       useSofaWithSwaggerUI({
        basePath: '/rest',
        swaggerUIEndpoint: '/swagger',
        servers: [
          {
            url: '/', // Specify Server's URL.
            description: 'Development server'
          }
        ],
        info: {
          title: 'Example API',
          version: '1.0.0'
        }
      })
    ],
    fetchAPI: createFetch({
      // We prefer `node-fetch` over `undici` and current unstable Node's implementation
      useNodeFetch: true,
      formDataLimits: {
        // Maximum allowed file size (in bytes)
        fileSize: 1000000,
        // Maximum allowed number of files
        files: 10,
        // Maximum allowed size of content (operations, variables etc...)
        fieldSize: 1000000,
        // Maximum allowed header size for form data
        headerSize: 1000000
      }
    })
  });

  const server = createServer(yoga)

  app.route({
    url: '/graphql',
    method: ['GET', 'POST', 'OPTIONS'],
    handler: async (req, reply) => {
      // Second parameter adds Fastify's `req` and `reply` to the GraphQL Context
      const response = await yoga.handleNodeRequest(req, {
        req,
        reply
      })
      response.headers.forEach((value, key) => {
        reply.header(key, value)
      })

      reply.status(response.status)

      reply.send(response.body)

      return reply
    }

  })

  // Serverless Lambda feature

  async function handler(
    event: APIGatewayEvent,
    lambdaContext: Context
  ): Promise<APIGatewayProxyResult> {
    const url = new URL(event.path, 'http://localhost')
    if (event.queryStringParameters != null) {
      for (const name in event.queryStringParameters) {
        const value = event.queryStringParameters[name]
        if (value != null) {
          url.searchParams.set(name, value)
        }
      }
    }

    const response = await yoga.fetch(
      url,
      {
        method: event.httpMethod,
        headers: event.headers as HeadersInit,
        body: event.body
          ? Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8')
          : undefined
      },
      {
        event,
        lambdaContext
      }
    )

    const responseHeaders: Record<string, string> = {}

    response.headers.forEach((value, name) => {
      responseHeaders[name] = value
    })

    return {
      statusCode: response.status,
      headers: responseHeaders,
      body: await response.text(),
      isBase64Encoded: false
    }
  }

  server.listen(4000, () => {
    console.info('Server is running on http://localhost:4000/graphql')
  })
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});
Enter fullscreen mode Exit fullscreen mode

you can see the gist here: https://gist.github.com/bastianhilton/31d8f516035a53c0e5575b80232d8934

Congratulations you have created a graphql server that supports Prisma, Typescript, envelop plugins, automatically generated rest apis with SOFA, Apollow Federation, and serverless deployment with Lambda.

Lets run the server from our root directory with ts-node index.ts

the end result will look like this:

Image description

Now you can plug and play this server into any nodejs environment to instantly add graphql for your databases.

With prisma you also have support for SQLite, SQL, SQL Server, Supabase, Postgresql, MySQL, MongoDB, CockroachDB, and Planetscale.

Top comments (0)