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"],
}
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:
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
:
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:
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
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:
Copy User ID
. Go to tab User
. Paste User ID
in the UserId field. Click the button Get User
. User information will be displayed:
Everything is working!
Top comments (0)