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"
]
}
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
After Removing the files and folders update pages/index.tsx
const IndexPage = () => (
<>
<h1>Hello Next.js 👋</h1>
</>
);
export default IndexPage;
Your Folder structure should look like this.
Now Run npm run dev
and go to localhost
You should see something like this in your browser
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"
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 (typically5432
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
}
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])
}
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"
You should see something like this
*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
}
}
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",
});
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"),
},
});
Create TypeDefs
bring in prisma by importing @prisma/client
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
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(),
})
},
})
*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 },
});
},
});
},
});
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"),
},
});
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",
});
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',
})
}
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);
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
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)