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
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 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()} {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, 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
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()} {new Date(time).toLocaleTimeString()}</p>
<label>Last Access Time:</label>
<p>{new Date(lastAccessed).toLocaleDateString()} {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, 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:
Top comments (0)