Simple full-stack application with GraphQL, Apollo 4, Node.js, Express, React, MongoDB, TypeScript
Tags: graphql, node, mongodb, react
Published: 2023-04-29 06:55:52

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:

- [Node.js](https://nodejs.org/en/download)

- [MongoDB](https://www.mongodb.com/try/download/community)

**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"],
}
```

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

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

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

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`))
```
The structure of the server side of the application:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rb1e8l0cgulwqchgvo4d.png)

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

```

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/io31d240o5gex4j5jwhf.png)

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

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

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;
```
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;
```
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;
```
The structure of the client side of the application:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m1o1xqz6zjzduhm3cq9f.png)

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"
    ]
  }
}

```

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j9ofp4csx07ttrc070q7.png)

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

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

Everything is working!