FullStack Nextjs with Typescript, Graphql
Tags: react, webdev, javascript, prisma
Published: 2020-09-16 20:16:19


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](https://en.wikipedia.org/wiki/Typesafe) and using [graphql ](https://graphql.org/). 

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

- [Typescript](https://www.typescriptlang.org/)
- [Nextjs](https://nextjs.org/)
- [Prisma ](https://www.prisma.io/)
- [Nexus ](https://nexusjs.org/#/components/schema/plugins/prisma)
- [PostgreSQL ](https://www.postgresql.org/) 

These are the main techs we are going to use. 

If You want to see the end result head on to this [repo](https://github.com/imran-ib/full-stack-nextjs)



## 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

```json
{
  "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"
  ]
}
```

**if you want to know more [checkout this repo](https://github.com/vercel/next.js/tree/canary/examples/with-typescript)**

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. 

```markdown
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
			
```

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

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

export default IndexPage;

```

Your Folder structure should look like this. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ggggxlc58xwtyz61hnso.png)


Now Run `npm run dev` and go to [localhost](http://localhost:3000/) 

You should see something like this in your browser

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/8016p9tbua2i2i4hgs61.png)

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

# API Routes 

[Nextjs Api Routes](https://nextjs.org/docs/api-routes/introduction) 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](https://www.prisma.io/)  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

```js
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
```

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](https://www.postgresql.org/docs/12/manage-ag-overview.html)
- `SCHEMA`: The name of the [schema](https://www.postgresql.org/docs/12/ddl-schemas.html) inside the database

in this example I will be using local Database. 



and `shcema.prisma` file 

```js
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 Prisma Schema 

```js
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])
}

```

Add These Scripts to your `package.json`

```js
"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"
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/78ox9giv6urwytluayic.png)
Now run `npm run dev:migrate `

You should see something like this 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/fu0lbu9egoinlc1jhpjq.png)


**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](https://www.prisma.io/docs/reference/database-connectors/postgresql)

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`

```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
  }
}
```

## 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**

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

const server = new ApolloServer();
  
  export default server.createHandler({
    path: "/api",
  });
  
```

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

in same file add following code

```js
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"),
  },
});

```

Create `TypeDefs`

bring in prisma by importing `@prisma/client` 

```js
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
```

**User and Post Model Type**

```js
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(),
    })
  },
})
```

**Mutation and Query **

```js


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 },
        });
      },
    });
  },
});
```

pass types to our schema

```js

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"),
  },
});
```

Now Your file should Look Like this

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

```js
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",
});


```



# 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` 

```js
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',
  })
}

```

 Now Go to `pages/index.ts` and import `WithApollo`  

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

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

export default withApollo(IndexPage);

```



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](https://dev-to-uploads.s3.amazonaws.com/i/c5q1n5f7xpebgxz0cwpc.png)


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.