Client, API, and database. A full-stack web app built entirely in Typescript.
Tags: node, typescript, react, express
Published: 2023-11-30 15:08:02

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](https://react.dev/), an API using [ExpressJS](https://expressjs.com/), and a database using [Sequelize](https://sequelize.org/).

**tl;dr**: The complete code can be found on [Github](https://github.com/Brandawg93/typescript-template).

---

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

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

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

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

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

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

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

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

Now, simply navigate to [http://localhost/ping](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
```

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

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

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

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

```
cd client
npm run start
```

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

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

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",
```

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\""
```

Ok, Let’s try it!

```
npm run dev
```

Now when you go to [http://localhost:3000](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](https://sequelize.org/) 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
```

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

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

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

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](https://expressjs.com/), [React](https://react.dev/), and [Sequelize](https://sequelize.org/). 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](https://eslint.org/) here:

[https://github.com/Brandawg93/typescript-template](https://github.com/Brandawg93/typescript-template)