Full-Stack Nextjs

Learn to Create Full-stack nextjs project with typescript and graphql
reactprismagraphqlnextjs
17-November-2020

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
  "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

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
const IndexPage = () => (
  <>
    <h1>Hello Next.js 👋</h1>
  </>
);

export default IndexPage;

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

1
2
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
  • SCHEMA: The name of the schema inside the database

in this example I will be using local Database.

and shcema.prisma file

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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

1
2
3
4
"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 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

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  /* 
    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

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
8
9
10
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

1
2
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();

User and Post Model Type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
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

1
2
3
4
5
6
7
8
9
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

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.

Get In Touch

Wanna get in touch or talk about a project?

Feel free to contact me via email at imran.ib@live.com

or leave a Message