In this tutorial, we’ll set up an Express server using the TypeScript language. We won’t use any frameworks — such as NestJS or Loopback — which means the initial setup will be a bit tedious.
However, we’ll get a taste of some basic concepts of object-oriented programming. Let’s get started.
First, we will need to have TypeScript installed as a global module. Run the following command:
npm i -g typescript
Verify that TypeScript was installed by running:
tsc -v
You should get back a message listing the version number of TypeScript installed. In my case, I got back the following:
Version 3.7.2
Open up a terminal window and navigate to whatever location in which you wish to start this project.
Then create the following directory, and change into it:
mkdir tserver
cd tserver
We’re going to create a number of directories, create an index.ts
file, and initialize our project as both an npm and TypeScript project. We’ll also install a few packages to get started. To do so, run the following commands:
npm init -y
tsc --init
mkdir src dist src/routes src/controllers src/config
touch src/index.ts src/config/constants.ts
npm i express
npm i -D concurrently nodemon @types/express
Open the project in Visual Studio Code:
code .
Open the tsconfig.json
file, uncomment the outDir
and rootDir
fields, and modify them as follows (they are on lines 15 and 16):
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}
tserver-tsconfig.js
The TypeScript compiler will look in the src
directory for our source code files and deposit the compiled JavaScript files into the dist
directory. We’ll leave the other options as they are.
Open the package.json
file and modify the scripts
as follows:
{
"name": "tserver",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start:dev": "nodemon dist/index.js",
"build:dev": "tsc --watch --preserveWatchOutput",
"dev": "concurrently \"npm:build:dev\" \"npm:start:dev\""
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1"
},
"devDependencies": {
"@types/express": "^4.17.2",
"concurrently": "^5.0.0",
"nodemon": "^1.19.4"
}
}
tserver-package.js
Concurrently allows us to run scripts in parallel, and nodemon will restart our server automatically whenever we save any changes we have made to our code. Let’s add in a line of code to our src/index.ts
file, and make sure our code runs:
echo "console.log('Hello');" > src/index.ts
npm run dev
The app will most likely crash the first time you run it. This is because the start
script starts running the app before the build
script can finish building the code.
Simply kill the server using Ctrl-C_,_ and restart it using npm run dev
. Your output should be similar to the following:
[start:dev] [nodemon] restarting due to changes...
[start:dev] [nodemon] starting `node dist/index.js`
[start:dev] Hello
[start:dev] [nodemon] clean exit - waiting for changes before restart
That’s all we need to do for the basic setup.
In the previous step, we installed the Express package as well as its required type definition files. Now we can get our server up and running.
First, let’s tell Express what port we want to use. Open src/config/constants.ts
, and add in the following:
export const PORT = process.env.PORT || 4000;
tserver-constants.ts
Now open up src/index.ts
, delete the console.log()
statement, and add in the following:
import express from 'express';
import { PORT } from './config/constants';
const app = express();
app.use(express.json());
app.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});
tserver-index.ts
You should something similar to the following in the terminal:
[start:dev] [nodemon] restarting due to changes...
[start:dev] [nodemon] starting `node dist/index.js`
[start:dev] Server is listening on port 4000
A typical app is composed of various entities that implement basic CRUD operations. Examples of entities include users, messages, and images. Because these operations vary for each type of entity, we’ll encapsulate these CRUD operations into a TypeScript class. Create a new file:
touch src/controllers/CrudController.ts
Open it in VS Code. We’re going to create a base CRUD class that’ll serve as the parent class for a specialized User
class.
This base class will be different from a normal class because it’ll be abstract. An abstract class is similar to a TypeScript interface and is another way of defining the shape of our objects.
Add in the following code to src/controllers/CrudController.ts
:
import { Request, Response } from 'express';
export abstract class CrudController {
public abstract create(req: Request, res: Response): void;
public abstract read(req: Request, res: Response): void;
public abstract update(req: Request, res: Response): void;
public abstract delete(req: Request, res: Response): void;
}
tserver-crud.ts
We have specified the types for our request (req
) and response (res
) objects and created four abstract methods.
An abstract class cannot be instantiated, i.e., we cannot create an instance of it. We must extend this class, which we’ll do soon. When we mark a method as abstract, it means the child class must provide an implementation for it. Let’s see how that works. Let’s setup a User
controller:
mkdir src/controllers/User
touch src/controllers/User/User.ts
Open the file src/controllers/User/User.ts
, and add in the following:
import { Request, Response } from 'express';
import { CrudController } from '../CrudController';
export class UserController extends CrudController {
}
rawtserver-usercont.ts
You’ll see an error. Click on UserController
, and a light bulb should appear with options to fix the error. Click on “Implement inherited abstract class.”
Your code should now contain the following:
import { Request, Response } from 'express';
import { CrudController } from '../CrudController';
export class UserController extends CrudController {
public create(req: Request<import("express-serve-static-core").ParamsDictionary>, res: Response): void {
throw new Error("Method not implemented.");
}
public read(req: Request<import("express-serve-static-core").ParamsDictionary>, res: Response): void {
throw new Error("Method not implemented.");
}
public update(req: Request<import("express-serve-static-core").ParamsDictionary>, res: Response): void {
throw new Error("Method not implemented.");
}
public delete(req: Request<import("express-serve-static-core").ParamsDictionary>, res: Response): void {
throw new Error("Method not implemented.");
}
}
tserver-usercont02.ts
I have formatted the file by adding/removing white space. Sometimes, TypeScript is smart enough to provide the exact types for our method arguments as specified in our base class, and sometimes it’ll generate something strange like the above.
This code will work just fine, however, as it’s merely being explicit about where the type definitions live in the node_modules
folder.
Now let’s add some code to our read
method for our User
, which corresponds to a GET request:
import { Request, Response } from 'express';
import { CrudController } from '../CrudController';
export class UserController extends CrudController {
public create(req: Request<import("express-serve-static-core").ParamsDictionary>, res: Response): void {
throw new Error("Method not implemented.");
}
public read(req: Request<import("express-serve-static-core").ParamsDictionary>, res: Response): void {
res.json({ message: 'GET /user request received' });
}
public update(req: Request<import("express-serve-static-core").ParamsDictionary>, res: Response): void {
throw new Error("Method not implemented.");
}
public delete(req: Request<import("express-serve-static-core").ParamsDictionary>, res: Response): void {
throw new Error("Method not implemented.");
}
}
tserver-usercont03.ts
Before we move on the our user routes, let’s create one more file to help organize our exports:
touch src/controllers/index.ts
Open this new file, and add in the following:
import { UserController } from './User/User';
const userController = new UserController();
export {
userController
};
tserver-controller-index.ts
Now that we have a way of handling a GET request, let’s add in routes for our User
entity. In the routes
directory, create a directory named User
and a file called User.ts
:
mkdir src/routes/User
touch src/routes/User/User.ts
Open the file src/routes/User/User.ts
, and add in the following:
import express, { Request, Response } from 'express';
import { userController } from '../../controllers';
export const router = express.Router({
strict: true
});
router.post('/', (req: Request, res: Response) => {
userController.create(req, res);
});
router.get('/', (req: Request, res: Response) => {
userController.read(req, res);
});
router.patch('/', (req: Request, res: Response) => {
userController.update(req, res);
});
router.delete('/', (req: Request, res: Response) => {
userController.delete(req, res);
});
tserver-user-router01.ts
We’re also going to organize our exports by creating a src/routes/index.ts
file and adding in the following:
touch src/routes/index.ts
import { router as userRouter } from './User/User';
export {
userRouter
};
tserver-router-index.ts
The last step we need to do is open up our src/index.ts
file and tell our Express server to use our new User
router:
import express from 'express';
import { PORT } from './config/constants';
import { userRouter } from './routes';
const app = express();
app.use(express.json());
app.use('/users', userRouter);
app.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});
tserver-index02.ts
First, make sure there are no errors in the terminal. Then, open up a new browser tab, and navigate to localhost:4000/users.
That’s all for this tutorial. In a future tutorial, I’ll demonstrate how to set up both MongoDB and PostgreSQL.
This tutorial was also a continuation of my previous tutorial, “TypeScript with Reach Crash Course,” which you can read here.
You can find the code for this tutorial here.
#nodejs #TypeScript #Express