DEV Community

Imran Irshad
Imran Irshad

Posted on

FullStack Nextjs with Typescript, Graphql

Scaffolding a full stack project with a front-end and back-end is a real pain and takes time. It is nice to have a starter project that will help you up and running with minimum effort. So In this post we will learn how to build a full-stack web application from scratch that is typesafe and using graphql .

So What technologies are we going to use in this post.

These are the main techs we are going to use.

If You want to see the end result head on to this repo

Create Nextjs Project

To Create a Nextjs Project run the following command

npx create-next-app full-stack-nextjs --use-npm -e with-typescript

npx is a CLI tool whose purpose is to make it easy to install and manage dependencies hosted in the npm registry.

create-next-app is a tool which will create nextjs project and install all the dependencies.

full-stack-nextjs is name of our project. Alternatively you can name your project whatever you like.

--use-npm use npm our default package manager

-e for exact npm packages

with-typescript the project will be pre configured with typescript.

Additional Packages

cd into full-stack-nextjs and run the following command to install additional packages.

npm install @nexus/schema nexus-prisma apollo-server-micro @apollo/react-hooks apollo-client apollo-cache-inmemory @apollo/react-ssr apollo-link-http apollo-link-schema ts-node graphql graphql-tag express @prisma/cli @prisma/client --save

open tsconfig.json remove everything and paste following code

{
  "compilerOptions": {
    /* 
      Note that the "module" setting will be overriden by nextjs automatically
      (cf. https://github.com/zeit/next.js/discussions/10780).
      If you need to change it, you should use the --compiler-options or provide a separate 
      tsconfig.json entirely.
    */
    "module": "esnext",
    "target": "ES2019",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "exclude": [
    "node_modules"
  ],
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx"
  ]
}
Enter fullscreen mode Exit fullscreen mode

if you want to know more checkout this repo

Your Folder structure should look like this. Don't worry if it is not the same because we are going to remove most of the files anyway.

components/
    Layout.tsx              --> remove this file
    List.tsx                --> remove this file
    ListDetail.tsx          --> remove this file
    ListItem.tsx            --> remove this file
interface/
    index.tsx
pages/
    api/
        users/              --> remove this folder
            index.tsx       
    users/                  --> remove this folder
        [id].tsx
        index.tsx 
    about.tsx               --> remove this file
    index.tsx
utils/                      --> remove this folder

Enter fullscreen mode Exit fullscreen mode

After Removing the files and folders update pages/index.tsx

const IndexPage = () => (
  <>
    <h1>Hello Next.js 👋</h1>
  </>
);

export default IndexPage;

Enter fullscreen mode Exit fullscreen mode

Your Folder structure should look like this.

Alt Text

Now Run npm run dev and go to localhost

You should see something like this in your browser

Alt Text

Front end of our app is ready to use . Let's create back end now.

API Routes

Nextjs Api Routes provide a straightforward solution to build your API with Next.js

Any file inside the folder pages/api is mapped to /api/* and will be treated as an API endpoint instead of a page. They are server-side only bundles and won't increase your client-side bundle size.

We Already have pages/api directory. We Don't need separate work environment for our backend.

let's get started with prisma

Prisma

Prisma is an open-source database toolkit

If You Did not install all the packages then please install Prisma client by running following command

npm install @prisma/cli @prisma/client --save-dev

After installation initialize prisma by following command

npx prisma init

After running above command prisma directory is created in root of our project that has two files init.

.evn for environment variable (make sure to include it in .gitignore)

schema.prisma for our prisma schema

.env file

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
Enter fullscreen mode Exit fullscreen mode

You now need to adjust the connection URL to point to your own database

postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA

  • USER: The name of your database user
  • PASSWORD: The password for your database user
  • PORT: The port where your database server is running (typically 5432 for PostgreSQL)
  • DATABASE: The name of the database
  • SCHEMA: The name of the schema inside the database

in this example I will be using local Database.

and shcema.prisma file

datasource db {
  provider = "postgresql"   //Database Alternatively you can use MySQL or SQLite 
  url      = env("DATABASE_URL") // url from .env file
}

generator client {
  provider = "prisma-client-js"  // To Genetate prisma client
}

Enter fullscreen mode Exit fullscreen mode

Add Prisma Schema

datasource db {
  provider = "postgresql"   //Database Alternatively you can use MySQL or SQLite 
  url      = env("DATABASE_URL") // url from .env file
}

generator client {
  provider = "prisma-client-js"  // To Genetate prisma client
}

// Add Two Model User and Post
model User {
  email    String  @unique
  password String
  id       Int     @default(autoincrement()) @id
  name     String?
  posts    Post[]
}

model Post {
  authorId  Int?
  content   String?
  id        Int     @default(autoincrement()) @id
  published Boolean @default(false)
  title     String
  author    User?   @relation(fields: [authorId], references: [id])
}

Enter fullscreen mode Exit fullscreen mode

Add These Scripts to your package.json

"generate": "npm -s run generate:prisma && npm -s run generate:nexus",
"dev:migrate": "prisma2 migrate save --experimental -c && prisma2 migrate up --experimental -c",
"generate:prisma": "prisma generate",
"generate:nexus": "ts-node --transpile-only -P nexus.tsconfig.json pages/api"
Enter fullscreen mode Exit fullscreen mode

Alt Text
Now run npm run dev:migrate

You should see something like this

Alt Text

*If You Don't see something like this and see an error message please make sure you added database credentials correctly * you can find more info here

To See visually our Models

Run npx prisma studio and visit http://localhost:5555/

In the root if the project create new file nexus.tsconfig.json

{
  /* 
    This file is used as a workaround for https://github.com/graphql-nexus/schema/issues/391
    It allows the nexus schema generation to work (done via `npm run generate:nexus`).
  */
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "dist",
    "strict": true,
    "lib": ["esnext"],
    "esModuleInterop": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Backend Server

in pages/api create new file index.ts

Let's Create server.

to create server we will use apollo-server-micro if you haven't already install npm install apollo-server-micro

Note: If You are copy pasting don't copy yet. I will leave a note when you should copy

import { ApolloServer } from 'apollo-server-micro'

const server = new ApolloServer();

  export default server.createHandler({
    path: "/api",
  });

Enter fullscreen mode Exit fullscreen mode

But Our Apollo Server need a schema. Lets create One.

in same file add following code

import { makeSchema } from "@nexus/schema";
import path from "path";

const schema = makeSchema({
  types: [], // we will create types later
  outputs: {
    typegen: path.join(process.cwd(), "pages", "api", "nexus-typegen.ts"),
    schema: path.join(process.cwd(), "pages", "api", "schema.graphql"),
  },
});

Enter fullscreen mode Exit fullscreen mode

Create TypeDefs

bring in prisma by importing @prisma/client

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
Enter fullscreen mode Exit fullscreen mode

User and Post Model Type

const User = objectType({
  name: 'User',
  definition(t) {
    t.int('id')
    t.string('name')
    t.string('email')
    t.list.field('posts', {
      type: 'Post',
      resolve: parent =>
        prisma.user
          .findOne({
            where: { id: Number(parent.id) },
          })
          .posts(),
    })
  },
})

const Post = objectType({
  name: 'Post',
  definition(t) {
    t.int('id')
    t.string('title')
    t.string('content', {
      nullable: true,
    })
    t.boolean('published')
    t.field('author', {
      type: 'User',
      nullable: true,
      resolve: parent =>
        prisma.post
          .findOne({
            where: { id: Number(parent.id) },
          })
          .author(),
    })
  },
})
Enter fullscreen mode Exit fullscreen mode

*Mutation and Query *



const Query = objectType({
  name: 'Query',
  definition(t) {
    t.field('post', {
      type: 'Post',
      args: {
        postId: stringArg({ nullable: false }),
      },
      resolve: (_, args) => {
        return prisma.post.findOne({
          where: { id: Number(args.postId) },
        })
      },
    })

    t.list.field('feed', {
      type: 'Post',
      resolve: (_parent, _args, ctx) => {
        return prisma.post.findMany({
          where: { published: true },
        })
      },
    })

    t.list.field('drafts', {
      type: 'Post',
      resolve: (_parent, _args, ctx) => {
        return prisma.post.findMany({
          where: { published: false },
        })
      },
    })

    t.list.field('filterPosts', {
      type: 'Post',
      args: {
        searchString: stringArg({ nullable: true }),
      },
      resolve: (_, { searchString }, ctx) => {
        return prisma.post.findMany({
          where: {
            OR: [
              { title: { contains: searchString } },
              { content: { contains: searchString } },
            ],
          },
        })
      },
    })
  },
})


const Mutation = objectType({
  name: "Mutation",
  definition(t) {
    t.field("signupUser", {
      type: "User",
      args: {
        name: stringArg(),
        email: stringArg({ nullable: false }),
        password: stringArg({ nullable: false }),
      },
      resolve: (_, { name, email, password }, ctx) => {
        return prisma.user.create({
          data: {
            name,
            email,
            password,
          },
        });
      },
    });

    t.field("deletePost", {
      type: "Post",
      nullable: true,
      args: {
        postId: stringArg(),
      },
      resolve: (_, { postId }, ctx) => {
        return prisma.post.delete({
          where: { id: Number(postId) },
        });
      },
    });

    t.field("createDraft", {
      type: "Post",
      args: {
        title: stringArg({ nullable: false }),
        content: stringArg(),
        authorEmail: stringArg(),
      },
      resolve: (_, { title, content, authorEmail }, ctx) => {
        return prisma.post.create({
          data: {
            title,
            content,
            published: false,
            author: {
              connect: { email: authorEmail },
            },
          },
        });
      },
    });

    t.field("publish", {
      type: "Post",
      nullable: true,
      args: {
        postId: stringArg(),
      },
      resolve: (_, { postId }, ctx) => {
        return prisma.post.update({
          where: { id: Number(postId) },
          data: { published: true },
        });
      },
    });
  },
});
Enter fullscreen mode Exit fullscreen mode

pass types to our schema


const schema = makeSchema({
  types: [Query, Mutation, Post, User],
  outputs: {
    typegen: path.join(process.cwd(), "pages", "api", "nexus-typegen.ts"),
    schema: path.join(process.cwd(), "pages", "api", "schema.graphql"),
  },
});
Enter fullscreen mode Exit fullscreen mode

Now Your file should Look Like this

Note: You can copy this code and paste it in your server.ts file

import { makeSchema, objectType, stringArg } from "@nexus/schema";
import { PrismaClient } from "@prisma/client";
import { ApolloServer } from "apollo-server-micro";
import path from "path";

const prisma = new PrismaClient();

const User = objectType({
  name: "User",
  definition(t) {
    t.int("id");
    t.string("name");
    t.string("email");
    t.list.field("posts", {
      type: "Post",
      resolve: (parent) =>
        prisma.user
          .findOne({
            where: { id: Number(parent.id) },
          })
          .posts(),
    });
  },
});

const Post = objectType({
  name: "Post",
  definition(t) {
    t.int("id");
    t.string("title");
    t.string("content", {
      nullable: true,
    });
    t.boolean("published");
    t.field("author", {
      type: "User",
      nullable: true,
      resolve: (parent) =>
        prisma.post
          .findOne({
            where: { id: Number(parent.id) },
          })
          .author(),
    });
  },
});

const Query = objectType({
  name: "Query",
  definition(t) {
    t.field("post", {
      type: "Post",
      args: {
        postId: stringArg({ nullable: false }),
      },
      resolve: (_, args) => {
        return prisma.post.findOne({
          where: { id: Number(args.postId) },
        });
      },
    });

    t.list.field("feed", {
      type: "Post",
      resolve: (_parent, _args, ctx) => {
        return prisma.post.findMany({
          where: { published: true },
        });
      },
    });

    t.list.field("drafts", {
      type: "Post",
      resolve: (_parent, _args, ctx) => {
        return prisma.post.findMany({
          where: { published: false },
        });
      },
    });

    t.list.field("filterPosts", {
      type: "Post",
      args: {
        searchString: stringArg({ nullable: true }),
      },
      resolve: (_, { searchString }, ctx) => {
        return prisma.post.findMany({
          where: {
            OR: [
              { title: { contains: searchString } },
              { content: { contains: searchString } },
            ],
          },
        });
      },
    });
  },
});

const Mutation = objectType({
  name: "Mutation",
  definition(t) {
    t.field("signupUser", {
      type: "User",
      args: {
        name: stringArg(),
        email: stringArg({ nullable: false }),
        password: stringArg({ nullable: false }),
      },
      resolve: (_, { name, email, password }, ctx) => {
        return prisma.user.create({
          data: {
            name,
            email,
            password,
          },
        });
      },
    });

    t.field("deletePost", {
      type: "Post",
      nullable: true,
      args: {
        postId: stringArg(),
      },
      resolve: (_, { postId }, ctx) => {
        return prisma.post.delete({
          where: { id: Number(postId) },
        });
      },
    });

    t.field("createDraft", {
      type: "Post",
      args: {
        title: stringArg({ nullable: false }),
        content: stringArg(),
        authorEmail: stringArg(),
      },
      resolve: (_, { title, content, authorEmail }, ctx) => {
        return prisma.post.create({
          data: {
            title,
            content,
            published: false,
            author: {
              connect: { email: authorEmail },
            },
          },
        });
      },
    });

    t.field("publish", {
      type: "Post",
      nullable: true,
      args: {
        postId: stringArg(),
      },
      resolve: (_, { postId }, ctx) => {
        return prisma.post.update({
          where: { id: Number(postId) },
          data: { published: true },
        });
      },
    });
  },
});

export const schema = makeSchema({
  types: [Query, Mutation, Post, User],
  outputs: {
    typegen: path.join(process.cwd(), "pages", "api", "nexus-typegen.ts"),
    schema: path.join(process.cwd(), "pages", "api", "schema.graphql"),
  },
});

export const config = {
  api: {
    bodyParser: false,
  },
};

export default new ApolloServer({ schema }).createHandler({
  path: "/api",
});


Enter fullscreen mode Exit fullscreen mode

Connect Our Backend to Our frontend with Apollo Client

In the Root of our project create new file apollo/clinet.js and paste following code.

Note You need these packages @apollo/react-hooks apollo-client apollo-cache-inmemory @apollo/react-ssr apollo-link-http apollo-link-schema

import React from 'react'
import Head from 'next/head'
import { ApolloProvider } from '@apollo/react-hooks'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'

let apolloClient = null

/**
 * Creates and provides the apolloContext
 * to a next.js PageTree. Use it by wrapping
 * your PageComponent via HOC pattern.
 * @param {Function|Class} PageComponent
 * @param {Object} [config]
 * @param {Boolean} [config.ssr=true]
 */
export function withApollo(PageComponent, { ssr = true } = {}) {
  const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
    const client = apolloClient || initApolloClient(apolloState)
    return (
      <ApolloProvider client={client}>
        <PageComponent {...pageProps} />
      </ApolloProvider>
    )
  }

  // Set the correct displayName in development
  if (process.env.NODE_ENV !== 'production') {
    const displayName =
      PageComponent.displayName || PageComponent.name || 'Component'

    if (displayName === 'App') {
      console.warn('This withApollo HOC only works with PageComponents.')
    }

    WithApollo.displayName = `withApollo(${displayName})`
  }

  if (ssr || PageComponent.getInitialProps) {
    WithApollo.getInitialProps = async ctx => {
      const { AppTree } = ctx

      // Initialize ApolloClient, add it to the ctx object so
      // we can use it in `PageComponent.getInitialProp`.
      const apolloClient = (ctx.apolloClient = initApolloClient())

      // Run wrapped getInitialProps methods
      let pageProps = {}
      if (PageComponent.getInitialProps) {
        pageProps = await PageComponent.getInitialProps(ctx)
      }

      // Only on the server:
      if (typeof window === 'undefined') {
        // When redirecting, the response is finished.
        // No point in continuing to render
        if (ctx.res && ctx.res.finished) {
          return pageProps
        }

        // Only if ssr is enabled
        if (ssr) {
          try {
            // Run all GraphQL queries
            const { getDataFromTree } = await import('@apollo/react-ssr')
            await getDataFromTree(
              <AppTree
                pageProps={{
                  ...pageProps,
                  apolloClient,
                }}
              />
            )
          } catch (error) {
            // Prevent Apollo Client GraphQL errors from crashing SSR.
            // Handle them in components via the data.error prop:
            // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
            console.error('Error while running `getDataFromTree`', error)
          }

          // getDataFromTree does not call componentWillUnmount
          // head side effect therefore need to be cleared manually
          Head.rewind()
        }
      }

      // Extract query data from the Apollo store
      const apolloState = apolloClient.cache.extract()
      return {
        ...pageProps,
        apolloState,
      }
    }
  }

  return WithApollo
}

/**
 * Always creates a new apollo client on the server
 * Creates or reuses apollo client in the browser.
 * @param  {Object} initialState
 */
function initApolloClient(initialState) {
  // Make sure to create a new client for every server-side request so that data
  // isn't shared between connections (which would be bad)
  if (typeof window === 'undefined') {
    return createApolloClient(initialState)
  }

  // Reuse client on the client-side
  if (!apolloClient) {
    apolloClient = createApolloClient(initialState)
  }

  return apolloClient
}

/**
 * Creates and configures the ApolloClient
 * @param  {Object} [initialState={}]
 */
function createApolloClient(initialState = {}) {
  const ssrMode = typeof window === 'undefined'
  const cache = new InMemoryCache().restore(initialState)

  return new ApolloClient({
    ssrMode,
    link: createIsomorphLink(),
    cache,
  })
}

function createIsomorphLink() {
  const { HttpLink } = require('apollo-link-http')
  return new HttpLink({
    uri: 'http://localhost:3000/api',
    credentials: 'same-origin',
  })
}

Enter fullscreen mode Exit fullscreen mode

Now Go to pages/index.ts and import WithApollo

import { withApollo } from "../apollo/client";

const IndexPage = () => (
  <>
    <h1>Hello Next.js 👋</h1>
  </>
);

export default withApollo(IndexPage);

Enter fullscreen mode Exit fullscreen mode

We have Script in package.json named generate

"generate": "npm -s run generate:prisma && npm -s run generate:nexus",

that command in responsible for generating types and schema.

After running this command you should see two file in your pages/api nexus-typegen.ts and schema.graphql

Now Let's Head on to http://localhost:3000/api

Alt Text

There you have it. you can carryon with this project to build your full stack application.

In the next post I will be showing you have you can implement authencation with this flow.

Top comments (0)