Backend | Part 1 — Deploying a Node.js Backend with TypeScript
Setup
npm init -y
touch index.js
In package.json
set "type": "module"
to enable ES6 modules.
{
"name": "backend",
"main": "index.js",
"scripts": {},
"author": "Josh Holloway",
"type": "module"
}
Libs
npm i -D nodemon
npm i express cors dotenv
Add scripts:
{
"name": "backend",
"main": "index.js",
"author": "Josh Holloway",
"type": "module",
"devDependencies": {
"nodemon": "^3.0.1"
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2"
},
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
}
}
Env vars
Note that we don't yet have an enviroment variable set for NODE_ENV
. Logging process.env.NODE_ENV
will result in undefined
.
console.log('NODE_ENV: ', process.env.NODE_ENV);
Create a .env file:
touch .env
NODE_ENV=development
PORT=5000
Then, in index.js:
import * as dotenv from 'dotenv'
dotenv.config()
console.log('NODE_ENV: ', process.env.NODE_ENV);
Local / remote repo
Create .gitignore
:
touch .gitignore
.env
.DS_store
node_modules
Create local repo and commit to remote repo:
git init
git branch -M main
git status
ls -lah
git add .
git commit -m
git remote add origin https://github.com/joshdotjs/backend
git push -u origin main
Determine Node.js version and set in engines property in package.json
.
node -v
npm -v
Add this to package.json:
"engines": {
"node": "18.15.0",
"npm": "9.5.0"
},
TypeScript
Add TypeScript and rimraf:
npm i -D typescript rimraf @types/node @types/express @types/cors
Add a build script in package.json
.
"scripts": {
"start": "node dst/index.js",
"dev": "tsc && nodemon dst/index.js",
"build": "rimraf && tsc"
}
We are using rimraf to clear out our /dst
folder on each build.
Create a /src
directory and place index.ts
inside.
Create a tsconfig.json
file:
touch tsconfig.json
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"sourceMap": true,
"outDir": "dst",
},
"include": ["src/**/*"],
}
"module": "NodeNext"
: This option specifies the module code generation method. The value "NodeNext" refers to the module resolution strategy optimized for Node.js with ECMAScript modules (as of a newer Node.js version that supports this)."moduleResolution": "NodeNext"
: This option sets the module resolution strategy. Setting it to "NodeNext" makes the compiler use a resolution strategy tailored to Node.js with ECMAScript modules."target": "ES2022"
: This option tells the TypeScript compiler to transpile the code to ECMAScript 2022 syntax. This means that the generated code will use features available in the ES2022 standard."sourceMap": true
: This flag enables the generation of source maps. Source maps help in debugging by mapping the compiled JavaScript back to the original TypeScript source code."outDir": "dst"
: This option specifies the directory where the compiled JavaScript files will be emitted. In this case, the output directory is named "dst.""include": ["src/**/*"]
: This option specifies a glob pattern for files that should be included in the compilation. Here, it includes all files within the "src" directory and its subdirectories.
ES Modules
Make sure to import files local to the project with their file extension.
- Imports from
node_modules
do not need a file extension. - Use
.cjs
for compiled.cts
TypeScript files that contain CommonJS module syntax. - Use
.js
for compiled.ts
TypeScript files that contain ES6 module syntax.
import x1 from './file.js';
import x2 from './file.cjs';
console.log(x1);
console.log(x2);
import express from 'express';
file.ts
:
export default "file.js";
file.cts
:
module.exports = "file.cjs";
Set up an endpoint and add a fake DB via fs
Let's create a temporary fake database utilizing the file system in order to test our setup.
touch count.txt
Inside /count.txt
write a single character of 1
. Our test code will read this number, increase its value by one, display this value, and update this file stored on the server with the new incremented value.
1
Below is our server test code inside/src/index.ts
.
import x1 from './file.js';
import x2 from './file.cjs';
console.log(x1);
console.log(x2);
import * as dotenv from 'dotenv';
dotenv.config()
console.log('NODE_ENV: ', process.env.NODE_ENV);
// const express = require('express');
import express, { Express, Request, Response } from 'express';
// const cors = require('cors');
import cors from 'cors';
import { readFileSync, writeFileSync } from 'fs';
// ==============================================
const server = express();
// middleware
server.use(express.json());
server.use(cors());
// ==============================================
server.use('/home', (req: Request, res: Response) => {
const count = readFileSync('count.txt', 'utf-8');
console.log('count: ', count);
const new_count = parseInt(count) + 1;
writeFileSync('count.txt', new_count.toString());
const html = `
<htnl>
<head></head>
<body style="height: 100vh; display: grid; place-items: center;">
<h1>Success with TypeScript!</h1>
<h5>This HTML is the response of a GET request to our deployed Node.js endpoint <code>/home</code>.</h5>
<p>count: ${new_count}</p>
</body>
</html>
`;
res.send(html);
});
// ==============================================
server.use('*', (req: Request, res: Response) => {
const html = `
<htnl>
<head></head>
<body>
<h1>404 - catch all endpoint</h1>
</body>
</html>
`;
res.send(html);
});
// ==============================================
const PORT = process.env.PORT ?? 5000;
server.listen(PORT, () => {
console.log(`listening on port: ${PORT}`);
});
Code + Demo
Conclusion
In this post, we set up a Node.js backend utilizing TypeScript. We'll use this base project as the starting point for our custom backend that will mainly be used as a REST API. We'll eventually add bcrypt for authentication and a payment system utilizing the Stripe API. In the next post, we'll add in a real Postgres database. Stay tuned 📺.