Algobook
- The developer's handbook
mode-switch
back-button
Buy Me A Coffee
Sat May 27 2023

Build a full stack application with React and NodeJs

In this tutorial, we will create a full stack application. We will set up a client with create-react-app and a server with NodeJs and express. We will then integrate our server with our Sport winner API to get some Wimbledon winners data to provide our client with.

Sounds fun, right?

Create our server

Let's begin with our server side. We will set up our project structure and install our dependencies before we write our code.

mkdir my-sport-api cd my-sport-api npm init -y npm install axios express cors touch index.js

And in our index.js file, we will add following code to set up our small API.

const express = require("express"); const cors = require("cors"); const axios = require("axios"); const app = express(); const PORT = 3001; app.use(cors({ origin: "http://localhost:3000" })); app.get("/winners", async (req, res) => { const response = await axios.get( `https://api.algobook.info/v1/sports/wimbledon` ); res.send({ data: response.data }); }); app.get("/winners/:year", async (req, res) => { const { year } = req.params; const response = await axios.get( `https://api.algobook.info/v1/sports/wimbledon/${year}` ); res.send({ data: response.data }); }); app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); });

Let's go through above code.

  • We are requiring our dependencies
  • We are initializing our app and specifying a port number
  • We are setting up some cors rules to only allow http://localhost:3000 to access the API
  • We are setting up our two endoints to get the winners of Wimbledon. One for getting all winners, and one for getting a specific winner for a year

Client side

Now, we will tie our backend together with a nice looking client. In the terminal, run following commands

npx create-react-app my-sport-client --template typescript

Then we will navigate to our App.tsx file and clear out all the code that are prefilled by the script and add our own stuff.

Let's begin with clearing out the unnecessary code and keep this:

import "./App.css"; function App() { return <div className="App"></div>; } export default App;

Create state

Let's set up our state for the App component, as well as our interfaces to specify the data we are getting from the API.

interface IPlayer { name: string; country: string; } export interface ITennisStats { year: string; setScores: string; winner: IPlayer; loser: IPlayer; } function App() { const [winners, setWinners] = useState<ITennisStats[] | null>(null); return <div className="App"></div>; }

Create fetch functions

Now, we will create two functions for calling our endpoints. One for getting all winners, and one for getting a specific year

const fetchAllWinners = async () => { const response = await fetch("http://localhost:3001/winners"); const winnerResponse = await response.json(); setWinners(winnerResponse.data); }; const fetchWinnerByYear = async (year: string | undefined) => { if (!year) { fetchAllWinners(); return; } const response = await fetch(`http://localhost:3001/winners/${year}`); const winnerResponse = await response.json(); const winner = winnerResponse.data.error ? [] : [winnerResponse.data]; setWinners(winner); };

Get default data

So, we want to present our users with some data to begin with. In order to do that, we will call our fetchAllWinners when the component is mounted using useEffect hook.

useEffect(() => { fetchAllWinners(); }, []);

App.tsx so far

Currently, our App.tsx looks like this

import { useState } from "react"; import "./App.css"; interface IPlayer { name: string; country: string; } export interface ITennisStats { year: string; setScores: string; winner: IPlayer; loser: IPlayer; } function App() { const [winners, setWinners] = useState<ITennisStats[] | null>(null); const fetchAllWinners = async () => { const response = await fetch("http://localhost:3001/winners"); const winnerResponse = await response.json(); setWinners(winnerResponse.data); }; const fetchWinnerByYear = async (year: string | undefined) => { if (!year) { fetchAllWinners(); return; } const response = await fetch(`http://localhost:3001/winners/${year}`); const winnerResponse = await response.json(); const winner = winnerResponse.data.error ? [] : [winnerResponse.data]; setWinners(winner); }; useEffect(() => { fetchAllWinners(); }, []); return <div className="App"></div>; } export default App;

Create our stats component

If you navigate to the browser, you will see a completely white page. Let's change that with a new component for displaying our data.

Create a new component called TennisStats.tsx in the src/ folder, along with a TennisStats.css file.

In our TennisStats.tsx, we will add an interface to specify the data we want our component to accept, and then we will render the data.

import { ITennisStats } from "./App"; import "./TennisStats.css"; interface ITennisStatsProps { data: ITennisStats; } export const TennisStats = (props: ITennisStatsProps) => { const { year, setScores, winner, loser } = props.data; return ( <div className="statsContainer"> <h2>{year}</h2> <div className="winner">{`Winner: ${winner.name} (${winner.country})`}</div> <div className="loser">{`Loser: ${loser.name} (${loser.country})`}</div> <div>{`Scores: ${setScores}`}</div> </div> ); };

Style the component

In our TennisStats.css file, we will create some simple styling to make it a little bit more appealing

.statsContainer { display: flex; flex-direction: column; border: 1px solid #ddd; border-radius: 4px; padding: 1rem; gap: 1rem; width: 200px; } .winner { color: #2ed22e; } .loser { color: #e33636; }

Render our component

Now it is time to actual get something on the screen. Let's create a function in App.tsx and render our stats component.

Start by importing our component in our App.tsx

import { TennisStats } from "./TennisStats";

Then, create renderTennisStats() function

const renderTennisStats = () => { if (winners?.length) { return winners.map((winner) => <TennisStats data={winner} />); } return null; };

And invoke it like this in the return clause

return <div className="App">{renderTennisStats()}</div>;

Full App.tsx code

Our full code should look like this now

import { useEffect, useState } from "react"; import "./App.css"; import { TennisStats } from "./TennisStats"; interface IPlayer { name: string; country: string; } export interface ITennisStats { year: string; setScores: string; winner: IPlayer; loser: IPlayer; } function App() { const [winners, setWinners] = useState<ITennisStats[] | null>(null); const fetchAllWinners = async () => { const response = await fetch("http://localhost:3001/winners"); const winnerResponse = await response.json(); setWinners(winnerResponse.data); }; const fetchWinnerByYear = async (year: string | undefined) => { if (!year) { fetchAllWinners(); return; } const response = await fetch(`http://localhost:3001/winners/${year}`); const winnerResponse = await response.json(); const winner = winnerResponse.data.error ? [] : [winnerResponse.data]; setWinners(winner); }; useEffect(() => { fetchAllWinners(); }, []); const renderTennisStats = () => { if (winners?.length) { return winners.map((winner) => <TennisStats data={winner} />); } return null; }; return <div className="App">{renderTennisStats()}</div>; } export default App;

App.css

Head in to App.css and remove all default styling and change the .App class to this:

.App { display: flex; flex-wrap: wrap; padding: 2rem; gap: 0.5rem; }

Result

In your browser, you should be seeing something like this:

example-image

Cool! We have now a fully functional full-stack application!

I will now continue with extending our App.tsx to make use of our second endpoint with a search input field.

Extend our App.tsx

Let's add some searching capability to our component, and let the user search for a specific year using an input. Create an input and a button in App.tsx

return ( <> <div className="inputContainer"> <input className="input" type="number" min={1968} max={2022} value={searchYear} onChange={(ev) => setSearchYear(ev.target.value)} /> <button onClick={() => fetchWinnerByYear(searchYear)}>Search</button> </div> <div className="App">{renderTennisStats()}</div> </> );

Add our searchYear state

const [searchYear, setSearchYear] = useState<string | undefined>();

Style our input

In App.css add two new classes

.inputContainer { display: flex; gap: 0.25rem; padding: 2rem; } .input { width: 200px; }

Handle bad input

If we have searched for a year that didn't result in any hits, we should tell the user so. Extend our renderTennisStats function:

const renderTennisStats = () => { if (winners?.length) { return winners.map((winner) => <TennisStats data={winner} />); } if (searchYear) { return `Nothing found for ${searchYear}`; } return null; };

Full code

And now we are done, our App.tsx should look like below code snippet

import { useEffect, useState } from "react"; import "./App.css"; import { TennisStats } from "./TennisStats"; interface IPlayer { name: string; country: string; } export interface ITennisStats { year: string; setScores: string; winner: IPlayer; loser: IPlayer; } function App() { const [winners, setWinners] = useState<ITennisStats[] | null>(null); const [searchYear, setSearchYear] = useState<string | undefined>(); const fetchAllWinners = async () => { const response = await fetch("http://localhost:3001/winners"); const winnerResponse = await response.json(); setWinners(winnerResponse.data); }; const fetchWinnerByYear = async (year: string | undefined) => { if (!year) { fetchAllWinners(); return; } const response = await fetch(`http://localhost:3001/winners/${year}`); const winnerResponse = await response.json(); const winner = winnerResponse.data.error ? [] : [winnerResponse.data]; setWinners(winner); }; useEffect(() => { fetchAllWinners(); }, []); const renderTennisStats = () => { if (winners?.length) { return winners.map((winner) => <TennisStats data={winner} />); } if (searchYear) { return `Nothing found for ${searchYear}`; } return null; }; return ( <> <div className="inputContainer"> <input className="input" type="number" min={1968} max={2022} value={searchYear} onChange={(ev) => setSearchYear(ev.target.value)} /> <button onClick={() => fetchWinnerByYear(searchYear)}>Search</button> </div> <div className="App">{renderTennisStats()}</div> </> ); } export default App;

Final result

And our final result with a search input, will look like this

final-result

Outro

Today we learned how we can built our own full stack application using React, NodeJs and even a third party API to display data for our users. We covered cors, GET endpoints, sending http requests and rendering response data in React.

I hope you liked this guide, and if you faced any issues, please don't hesitate to contact us here.

Thanks for reading!

signatureSat May 27 2023
See all our articles