DEV Community

Uku Lele
Uku Lele

Posted on • Updated on

Simple full-stack application with GraphQL, Apollo 4, Node.js, Express, React, MongoDB, TypeScript

We will create a simple full-stack application. On the client (in the browser), we will display information about users. We will be able to add a user to the database and get user information by user id. On the client side, we will use React, GraphQL, Apollo Client, TypeScript. On the server side we will use GraphQL, Apollo Server 4, TypeScript, Node.js, Express. The data will be stored in the database MongoDB.

Required Software
The computer must have the following software installed:

Beginning
Create a new directory for your app:
mkdir ragnemt-app

Server
Create a server directory under the ragnemt-app directory:
cd ragnemt-app
mkdir server

Go to the server directory, create a package.json file
and install Apollo Server and GraphQL:
cd server
npm init -y
npm i @apollo/server graphql

Install express, body-parser, @types/cors and @types/body-parser:
npm i express body-parser @types/cors @types/body-parser

Install Apollo data source, mongoose for MongoDB:
npm i apollo-datasource-mongodb mongoose

Install TypeScript:
npm i --save-dev typescript @types/node
Create a tsconfig.json file in the server folder. Add the following configuration to the file:

{
    "compilerOptions": {
        "rootDirs": [
            "src"
        ],
        "outDir": "dist",
        "lib": [
            "es2020"
        ],
        "target": "es2020",
        "module": "esnext",
        "moduleResolution": "node",
        "esModuleInterop": true,
        "types": [
            "node"
        ],
    },
    "include": ["src"],
}
Enter fullscreen mode Exit fullscreen mode

Create a src directory under the server directory.
mkdir src
cd src

Create a types.ts file in the _src _directory:

import { ObjectId } from 'mongodb';

export interface UserDocument {
  _id?: ObjectId
  username: string
  password: string
  email: string
}

export interface Context {
  loggedInUser: UserDocument
}
Enter fullscreen mode Exit fullscreen mode

Create a schema and convert it to a model. Create a models directory in the src directory. Create a user.ts file in the models directory:

import mongoose, { Schema } from "mongoose";
import { ObjectId } from 'mongodb';

const userSchema = new Schema({
  _id: ObjectId,
  username: String,
  password: String,
  email: String,
});

export const User = mongoose.model("user", userSchema);
Enter fullscreen mode Exit fullscreen mode

Create a dataSources directory in the src directory. Create a users.ts file in the dataSources directory:

import { MongoDataSource } from 'apollo-datasource-mongodb';
import { ObjectId } from 'mongodb';
import { UserDocument, Context } from '../types';

export default class Users extends MongoDataSource<UserDocument, Context> {

  getUsers() {
    return this.collection.find().toArray();
  }

  getUser(userId: ObjectId) {
    return this.collection.findOne({_id: new ObjectId(userId)});
  }

  addUser(username: string, password: string, email: string) {
    return this.collection.insertMany([{username, password, email}]);
  }
}
Enter fullscreen mode Exit fullscreen mode

Create a index.ts file in the directory src and make it look like this:

import mongoose, { ObjectId } from 'mongoose';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { GraphQLError } from 'graphql';
import express from 'express';
import http from 'http';
import cors from 'cors';
import { User as UserModel } from './models/user.js';
import Users from './dataSources/users.js';
import pkg from 'body-parser';
import { UserDocument } from './types.js';
const { json } = pkg;

await mongoose.connect('mongodb://127.0.0.1:27017/ragnemt');

const app = express();
// Our httpServer handles incoming requests to our Express app.
// Below, we tell Apollo Server to "drain" this httpServer,
// enabling our servers to shut down gracefully.
const httpServer = http.createServer(app);

const typeDefs = `#graphql
  type User {
    _id: ID!
    username: String
    password: String
    email: String
  }
  type Query {
    users: [User]
    user(_id: ID!): User
  }
  type Mutation {
    addUser(username: String, password: String, email: String): User
  }
`;

const resolvers = {
  Query: {
    users: (_parent: any, _args: any, { dataSourses }) => {
      return  dataSourses.users.getUsers();
    },
    user: (_parent: any, { _id }, { dataSourses }) => {
      return dataSourses.users.getUser(_id)
        .then((res: UserDocument) => {
          if (!res) {
            throw new GraphQLError(
              `User with User Id ${_id} does not exist.`
            );
          }
          return res;
        });
    },
  },
  Mutation: {
    addUser: (_parent: any, { username, password, email }, { dataSourses }) => {
      return dataSourses.users.addUser(username, password, email)
        .then((res: { insertedIds: ObjectId[]; }) => ({ _id: res.insertedIds[0], username, password, email }))
    }
  }
}

interface MyContext {
  dataSources?: {
    users: Users;
  };
}

const server = new ApolloServer<MyContext>({
  typeDefs,
  resolvers,
  plugins: [
    // Proper shutdown for the HTTP server.
    ApolloServerPluginDrainHttpServer({ httpServer }),
  ],
});
await server.start();

app.use(
  cors<cors.CorsRequest>(),
  json(),
  expressMiddleware(server, {
    context: async ({ req }) => ({
      token: req.headers.token,
      dataSourses: {
        users: new Users(await UserModel.createCollection())
      }
    }),
  }),
);

app.listen({ port: 4000 }, () => console.log(`🚀 Server ready at http://localhost:4000`))
Enter fullscreen mode Exit fullscreen mode

The structure of the server side of the application:

Image description

Add the following changes to package.json file:
"type": "module",
"scripts": {
"build": "tsc",
"start": "nodemon --exec ts-node-esm ./src/index.ts"
},

The configuration of the package.json file:

{
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "nodemon --exec ts-node-esm ./src/index.ts"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@apollo/server": "^4.7.0",
    "@types/body-parser": "^1.19.2",
    "@types/cors": "^2.8.13",
    "apollo-datasource-mongodb": "^0.5.4",
    "body-parser": "^1.20.2",
    "express": "^4.18.2",
    "graphql": "^16.6.0",
    "mongoose": "^7.1.0"
  },
  "devDependencies": {
    "@types/node": "^18.16.2",
    "nodemon": "^2.0.22",
    "ts-node": "^10.9.1",
    "typescript": "^5.0.4"
  }
}

Enter fullscreen mode Exit fullscreen mode

Install nodemon and ts-node:
npm i --save-dev nodemon ts-node
Run npm start:

Image description

The server side is working!

Client
Create a client directory under the ragnemt-app directory.
Go to the client directory. Start a new Create React App project with TypeScript:
npx create-react-app . --template typescript

Install Apollo Client and GraphQL:
npm i @apollo/client graphql

Change index.tsx to:

import ReactDOM from 'react-dom/client';
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
import './index.css';
import App from './App';

const client = new ApolloClient({
  uri: 'http://localhost:4000/',
  cache: new InMemoryCache(),
});

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);
Enter fullscreen mode Exit fullscreen mode

Change index.css to:

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

td,
th {
    border: 1px solid rgb(190, 190, 190);
    padding: 10px;
}

th[scope='col'] {
  background-color: #696969;
  color: #fff;
}

th[scope='row'] {
  background-color: #d7d9f2;
}

td {
    text-align: center;
}

tr:nth-child(even) {
  background-color: #eee;
}

caption {
  padding: 10px;
  /* caption-side: bottom; */
}

table {
  border-collapse: collapse;
  border: 2px solid rgb(200, 200, 200);
  letter-spacing: 1px;
  font-family: sans-serif;
  font-size: 0.8rem;
}
Enter fullscreen mode Exit fullscreen mode

Change App.tsx to:

import AddUser from './components/AddUser';
import GetUsers from './components/GetUsers';
import GetUser from './components/GetUser';
import './App.css';

const App = () => {

  const OnChangeTab = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>, activeTab: string) => {
    const tabcontent = document.getElementsByClassName("tabcontent");
    for (let i = 0; i < tabcontent.length; i++) {
      tabcontent[i].setAttribute("style","display:none");
    }
    const tablinks = document.getElementsByClassName("tablinks");
    for (let i = 0; i < tablinks.length; i++) {
      tablinks[i].className = tablinks[i].className.replace(" active", "");
    }
    document.getElementById(activeTab)?.setAttribute("style","display:block");
    e.currentTarget.className += " active";
  }

  return (
    <>
      <div className="tab">
        <button className="tablinks active" onClick={(e) => OnChangeTab(e, 'Users')}>Users</button>
        <button className="tablinks" onClick={(e) => OnChangeTab(e, 'User')}>User</button>
      </div>
      <div id="Users" className="tabcontent">
        <h3>Users</h3>
        <AddUser />
        <GetUsers />
      </div>

      <div id="User" className="tabcontent" style={{display: 'none'}}>
        <h3>User</h3>
        <GetUser />
      </div>

    </>
  )

}

export default App;
Enter fullscreen mode Exit fullscreen mode

Change App.css to:

/* Style the tab */
.tab {
  overflow: hidden;
  border: 1px solid #ccc;
  background-color: #f1f1f1;
}

/* Style the buttons inside the tab */
.tab button {
  background-color: inherit;
  float: left;
  border: none;
  outline: none;
  cursor: pointer;
  padding: 14px 16px;
  transition: 0.3s;
  font-size: 17px;
}

/* Change background color of buttons on hover */
.tab button:hover {
  background-color: #ddd;
}

/* Create an active/current tablink class */
.tab button.active {
  background-color: #ccc;
}

/* Style the tab content */
.tabcontent {
  padding: 6px 12px;
  border: 1px solid #ccc;
  border-top: none;
}
Enter fullscreen mode Exit fullscreen mode

Create a graphql directory under the src directory.
Create a queries.ts file in the graphql directory:

import { gql } from "@apollo/client";

export const GET_USERS = gql`
query GetUsers {
  users {
    _id
    username
    password
    email
  }
}
`;

export const GET_USER = gql`
query getUser ($id: ID!) {
  user(_id: $id) {
    _id
    username
    email
    password
  }
}
`;

export const ADD_USER = gql`
mutation AddUser($username: String, $password: String, $email: String) {
  addUser(username: $username, password: $password, email: $email){
        username
        password
        email
  }
}
`;
Enter fullscreen mode Exit fullscreen mode

Create a components directory under the src directory.
Create a AddUser.tsx file in the components directory:

import { useMutation } from '@apollo/client';
import {GET_USERS, ADD_USER} from '../graphql/queries';

const AddUser = () => {
    let inputName: HTMLInputElement | null;
    let inputPassword: HTMLInputElement | null;
    let inputEmail: HTMLInputElement | null;

    const [addUser, { loading, error, client }] = useMutation(ADD_USER);

    if (loading) return <p>'Submitting...'</p>;

    return (
      <div>
        {error && <p>`Submission error! ${error.message}`</p>}
        <form
          onSubmit={e => {
            e.preventDefault();
            addUser({
              variables: { username: inputName?.value, password: inputPassword?.value, email: inputEmail?.value },
              refetchQueries: [{ query: GET_USERS }],      
              onError: () => client.refetchQueries({ include: [GET_USERS] })
            });
          }}
        >
          <input
            ref={node => { inputName = node; }}
            placeholder='Name'
            required
          />
          <input
            ref={node => { inputPassword = node; }}
            placeholder='Password'
            required
          />
          <input
            type='email'
            ref={node => { inputEmail = node; }}
            placeholder='Email'
            required
          />
          <button type="submit">Add User</button>
        </form>
      </div>
    );
  }

  export default AddUser;
Enter fullscreen mode Exit fullscreen mode

Create a GetUser.tsx file in the components directory:

import { useLazyQuery } from '@apollo/client';
import { GET_USER } from '../graphql/queries';

const GetUser = () => {
  let inputId: HTMLInputElement | null = null;
  const [getUser, { loading, error, data }] = useLazyQuery(GET_USER);

  if (loading) return <p>'Submitting...'</p>;

  return (
    <div>
      {error && <p>`Submission error! ${error.message}`</p>}
      <form
        onSubmit={e => {
          e.preventDefault();
          getUser({ variables: { id: inputId?.value } })
        }}
      >
        <input
          ref={node => { inputId = node; }}
          placeholder='UserId'
          required
        />
        <button type="submit">Get User</button>
        {data?.user &&
          <table style={{ minWidth: 300 }}>
            <caption>User</caption>
            <tbody>
              <tr>
                <th scope="row">User ID</th>
                <td>{data?.user._id}</td>
              </tr>
              <tr>
                <th scope="row">User name</th>
                <td>{data?.user.username}</td>
              </tr>
              <tr>
                <th scope="row">User email</th>
                <td>{data?.user.email}</td>
              </tr>
            </tbody>
          </table>
        }
        <p></p>


      </form>
    </div>
  );
};

export default GetUser;
Enter fullscreen mode Exit fullscreen mode

Create a GetUsers.tsx file in the components directory:

import { useQuery } from '@apollo/client';
import { GET_USERS } from '../graphql/queries';
const GetUsers = () => {
  const { data, loading, error } = useQuery(GET_USERS);

  if (loading) return <p>...Loading</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <table>
      <caption>Users</caption>
      <thead>
        <tr>
          <th scope="col">User ID</th>
          <th scope="col">User name</th>
          <th scope="col">User email</th>
        </tr>
      </thead>
      <tbody>
        {data.users.map(({ _id, username, email }: { _id: string, username: string, email: string }) => (
          <tr key={_id}>
            <td>{_id}</td>
            <td>{username}</td>
            <td>{email}</td>
          </tr>
        ))}
      </tbody>
    </table>
  )
}

export default GetUsers;
Enter fullscreen mode Exit fullscreen mode

The structure of the client side of the application:

Image description

The configuration of the package.json file:

{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@apollo/client": "^3.7.13",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "@types/jest": "^27.5.2",
    "@types/node": "^16.18.25",
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.1",
    "graphql": "^16.6.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "typescript": "^4.9.5",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Enter fullscreen mode Exit fullscreen mode

Run npm start. We can now view client in the browser: http://localhost:3000

Image description

Enter Good Man in the Name field. Enter 123 in the Password field. Enter good_man@gm.net in the Email field. Click the button Add User. The information will be displayed in the users table:

Image description

Copy User ID. Go to tab User. Paste User ID in the UserId field. Click the button Get User. User information will be displayed:

Image description

Everything is working!

Top comments (0)