DEV Community

Cover image for Client, API, and database. A full-stack web app built entirely in Typescript.
Brandon McFarlin
Brandon McFarlin

Posted on

Client, API, and database. A full-stack web app built entirely in Typescript.

Many web applications of the past required knowledge of multiple languages and architectures to stand up a fully working service. But newer languages have emerged allowing developers to consolidate their knowledge to a single language. Let’s take a look at the technologies provided in Typescript that will allow us to create a client using React, an API using ExpressJS, and a database using Sequelize.

tl;dr: The complete code can be found on Github.


API

Let’s start with the API. Why? Because it’s the backbone of our application of course! And more importantly, the structure will make sense as we use predefined setup scripts to create the project.

Let’s begin by initializing the ExpressJS project:

mkdir typescript-template
cd typescript-template
npm init -y
Enter fullscreen mode Exit fullscreen mode

This will create a basic package.json file that we will be able to modify later.

{
  "name": "typescript-template",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
Enter fullscreen mode Exit fullscreen mode

From here, let’s begin installing the necessary packages required to run ExpressJS and typescript:

npm install --save cors express dotenv
npm install --save-dev @types/cors @types/express @types/node concurrently nodemon typescript
Enter fullscreen mode Exit fullscreen mode

This will install the required packages and add them to our package.json file.

Next let’s create our tsconfig.json file to tell typescript how we want our code compiled.

{
  "compilerOptions": {
    "target": "es2016",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    "module": "commonjs",                                /* Specify what module code is generated. */
    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
    "strict": true,                                      /* Enable all strict type-checking options. */
    "skipLibCheck": true,                                /* Skip type checking all .d.ts files. */
    "rootDir": "server",
    "outDir": "dist",
  },
  "include": [
    "server"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Most of these options are defaults if you run tsc --init with the notable exception of “rootDir”, “outDir”, and the “include” section. “rootDir” will tell typescript where our application code is, “outDir” will tell typescript where to compile the code, and “include” will tell typescript to only include the files within our specified folder (i.e. don’t include the client folder that we will make later).

Now that typescript knows what to do and where to look for our code, let’s create that server folder.

mkdir server
Enter fullscreen mode Exit fullscreen mode

Within this folder, let’s create a simple express file called server.ts and add some basic logic for starting express.

import express, { Express, Request, Response } from 'express';
import dotenv from 'dotenv';
import path from 'path'; // We will use this later
import cors from 'cors'; // We will use this later

dotenv.config();

const app: Express = express();
const port = process.env.WEB_PORT || 8080;

app.use(express.json());

app.get('/ping', (req: Request, res: Response) => {
  res.send('pong');
});

app.listen(port, async () => {
  console.log(`⚡️[typescript-template]: Server is running at http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

This sets up a simple express application on port 8080 that will reply pong to our /ping request. Before trying it out, let’s add some scripts to our package.json to make building and deploying a bit easier.

{
  "name": "typescript-template",
  "version": "1.0.0",
  "description": "",
  "main": "dist/server.js",
  "scripts": {
    "clean": "rm -rf dist",
    "server": "concurrently \"tsc --watch\" \"nodemon server/**/*\"",
    "build": "tsc && npm run build --prefix client",
    "start": "node dist/server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.3.1",
    "express": "^4.18.2"
  },
  "devDependencies": {
    "@types/cors": "^2.8.16",
    "@types/express": "^4.17.21",
    "@types/node": "^20.9.2",
    "concurrently": "^8.2.2",
    "nodemon": "^3.0.1",
    "typescript": "^5.2.2"
  }
}
Enter fullscreen mode Exit fullscreen mode

First, we update our "main” value to dist/server.js Then we add the necessary scripts. The clean script removes build files, the server script starts a development run of the application that will automatically rebuild as files are changed, the build script builds the project for production, and finally, the start script starts the application from the files from the production build. So typically, you would want to develop with server and deploy to production and use start. With all that done, let’s give it a shot!

npm run server
Enter fullscreen mode Exit fullscreen mode

Now, simply navigate to http://localhost/ping in your browser. You should see pong. If so, yay! Your server is set up. If not, check the console to see any errors you may have.


Client

Next, let’s work on our client code. For this, we will be using react’s standard startup script. We will also install everything inside a client folder to keep this code separate from the server.

npx create-react-app client --template typescript
Enter fullscreen mode Exit fullscreen mode

This will generate all of the basic code needed to start up our client with no modifications required! Well…not yet anyways. Let’s run the client to see it in action.

cd client
npm run start
Enter fullscreen mode Exit fullscreen mode

You should now see a basic react app telling you to edit App.tsx. If so, awesome! If not, check the logs for any errors.

Now, let’s hook up the client and server. First let’s create a new api endpoint in the server that displays the current time from the server. Add the following in server.ts.

app.get('/api/time', (req: Request, res: Response) => {
  res.send(JSON.stringify({ time: new Date()}));
});
Enter fullscreen mode Exit fullscreen mode

Next, let’s set up the client to call this endpoint. Modify App.tsx to look like the following:

import React from 'react';
import logo from './logo.svg';
import './App.css';

const getTime = async () => {
  const response = await fetch('/api/time');
  const body = await response.json();
  if (response.status !== 200) {
    throw Error(body.message);
  }
  return body;
}

function App() {
  const [time, setTime] = React.useState('');
  React.useEffect(() => {
    getTime()
      .then(res => setTime(res.time))
      .catch(err => console.log(err));
  }, []);
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>{new Date(time).toLocaleDateString()}&nbsp;{new Date(time).toLocaleTimeString()}</p>
      </header>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

In the code, we added a getTime method that calls the same endpoint and displays it on the page. Let’s try it! Start the client and server in two different shells and refresh the page. (We will make this easier later I promise!)

npm run start
Enter fullscreen mode Exit fullscreen mode
cd client
npm run start
Enter fullscreen mode Exit fullscreen mode

And it worked perfectly right? Wrong… What does your browser say? Probably Cannot GET /. We’ve hooked up the api call, but we forgot to hook up the actual files created by the client to be used by the server. Let’s fix that now. We will need to make a few modifications.

First, we need to point the express server to the build files created by react. Add this to your server.ts just below app.use(express.json());:

app.use(express.static(path.join(__dirname, '../client/build')));
app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, '../client/build/index.html'));
});
Enter fullscreen mode Exit fullscreen mode

We also need to make sure the server is happy with requests coming from a different base uri since different ports (e.g. 8080 and 3000) can cause a CORS issue. Add the following before what we just added above:

if (process.env.NODE_ENV !== 'production') {
  app.use(cors());
}
Enter fullscreen mode Exit fullscreen mode

Next we need to make sure the client will proxy requests from port 3000 to port 8080. Let’s add the following to our CLIENT’S package.json.

"proxy": "http://localhost:8080",
Enter fullscreen mode Exit fullscreen mode

Finally, let’s make our scripts a bit more convenient so we don’t have to run multiple shells. Add the following to your SERVER’S package.json.

"client": "npm start --prefix client",
"dev": "concurrently \"npm run server\" \"npm run client\""
Enter fullscreen mode Exit fullscreen mode

Ok, Let’s try it!

npm run dev
Enter fullscreen mode Exit fullscreen mode

Now when you go to http://localhost:3000, you will see the date and time displayed on the page. Woohoo!


Database

Finally let’s create a database. I will be using Sequelize to create a simple database in memory, but other options can be used such as sqlite or mysql. For this tutorial, let’s create a database that stores the last time the server is accessed and can retrieve the latest access time. Let’s begin by installing the necessary packages.

npm install --save sequelize sqlite3
Enter fullscreen mode Exit fullscreen mode

Note that more packages may be needed if you decide to use a different database. Now let’s create a file called database.ts within our server folder and initialize it with the following:

import { Sequelize, Model, DataTypes } from 'sequelize';

class ServerAccess extends Model {
  public id!: number;
  public lastAccessed!: Date;
}

const sequelize = new Sequelize('sqlite::memory:');


ServerAccess.init(
  {
    id: {
      type: DataTypes.INTEGER,
      autoIncrement: true,
      primaryKey: true,
    },
    lastAccessed: {
      type: DataTypes.DATE,
      allowNull: false,
    },
  },
  {
    sequelize,
    modelName: 'ServerAccess',
  }
);

sequelize.sync();

export async function updateLastAccessed(): Promise<void> {
  const serverAccess = await ServerAccess.findOne();
  if (serverAccess) {
    serverAccess.lastAccessed = new Date();
    await serverAccess.save();
  } else {
    await ServerAccess.create({ lastAccessed: new Date() });
  }
}

export async function getLastAccessed(): Promise<Date | null> {
  const serverAccess = await ServerAccess.findOne();
  return serverAccess ? serverAccess.lastAccessed : null;
}
Enter fullscreen mode Exit fullscreen mode

And…well…That’s pretty much it! We’ve just told Sequelize to create a database with an auto incrementing id and the last access time. We also have two exported functions to update and get that time. Simple!


Putting it all together

Now that we have the client, API, and database working, let’s put all that we’ve learned together by displaying the last time the server was accessed (via /api/time) on our react client app. First, let’s modify the server to retrieve the last access time AND THEN update the access time.

app.get('/api/time', async (req: Request, res: Response) => {
  const lastAccessed = (await getLastAccessed()) || new Date();
  res.send(JSON.stringify({ time: new Date(), lastAccessed: new Date(lastAccessed) }));
  await updateLastAccessed();
});
Enter fullscreen mode Exit fullscreen mode

With these changes, we will now initialize lastAccessed to either the last access time or the current datetime if the server has never been accessed. Once we send the response to the client, we update the last access time. Finally, let’s update the App function within App.tsx in the client to display the last access time.

function App() {
  const [time, setTime] = React.useState('');
  const [lastAccessed, setLastAccessed] = React.useState('');
  React.useEffect(() => {
    getTime()
      .then(res => {
        setTime(res.time);
        setLastAccessed(res.lastAccessed);
      })
      .catch(err => console.log(err));
  }, []);
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <label>Current Time:</label>
        <p>{new Date(time).toLocaleDateString()}&nbsp;{new Date(time).toLocaleTimeString()}</p>
        <label>Last Access Time:</label>
        <p>{new Date(lastAccessed).toLocaleDateString()}&nbsp;{new Date(lastAccessed).toLocaleTimeString()}</p>
      </header>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

That’s it! Simply run npm run dev again and you should see the dates and times update accordingly.


Conclusion

In this tutorial, we learned to create a full-stack web application completely in Typescript! We utilized some great frameworks to get us started like ExpressJS, React, and Sequelize. We are now able to use a small set of scripts to build, test, develop, and deploy our entire codebase.

The complete code for this tutorial can be found on Github along with a few other tools like ESLint here:

https://github.com/Brandawg93/typescript-template

Top comments (0)