How to Build REST API with TypeScript using only native modules
Tags: typescript
Published: 2022-03-07 17:12:53

**Written by [Rose Chege](https://blog.logrocket.com/author/rosechege/)✏️**

[TypeScript](https://blog.logrocket.com/tag/typescript/) is a typed superset of JavaScript that compiles to plain JavaScript during runtime. The dynamic nature of JavaScript does not allow you to catch any unexpected results unless the program has been executed, so the TypeScript type system will enable you to catch such errors at compile time instead of runtime. This means that any valid JavaScript code is also equivalent and valid TypeScript code.

TypeScript is both a language and a set of tools. As a language, it comprises syntax, keywords, and [type annotations](https://blog.logrocket.com/types-vs-interfaces-in-typescript/). Its toolset provides type information that the IDE can use to supply autocompletion, type hinting, refactoring options, static typing, and other features.

The TypeScript and JavaScript ecosystem offers diverse tools and libraries that help you build your application faster. For example, you will probably use the [Express](https://blog.logrocket.com/express-middleware-a-complete-guide/) library to set up your API when creating a server. Express provides you with functions to set up and manage your HTTP server at scale.

However, these libraries are developed using vanilla JavaScript. Behind the scenes, your Express server runs on raw TypeScript or JavaScript. This means the functions and methods that Express provides abstract the low-level logic of the vanilla base language. Your server does not directly interact with the base logic that the TypeScript or JavaScript provides; instead, Express accesses the vanilla code and transforms it to scalable code that lets you reduce your codebase.

Using these libraries speeds up your development time while reducing redundant code; the advantage they provide is undisputed. However, you might want to leverage the “bare bones” of TypeScript and run your apps using the vanilla code. This will help you execute the purest server without using libraries such as Express.

This guide will demonstrate how to use TypeScript to create a REST API using only the native modules. The project aims to help you learn how to make an HTTP server in Node.js without using additional libraries.

## How to set up TypeScript with Node.js

This tutorial will use Node to run TypeScript. Node is a JavaScript runtime designed to create scalable asynchronous event-driven applications.

Go ahead and install [Node runtime](https://nodejs.org/en/) on your computer. Then, create a project folder and initialize your project using `npm init -y`.

Let's configure Node to run TypeScript code. You need TypeScript dependencies available for Node. To do so, install the TypeScript Node package using the following command:

```shell
npm install -D typescript
```

You can now utilize your TypeScript project using `tsc --init`. This will create a `tsconfig.json` file with the default TypeScript compile options.

*> `tsc --init` may require you to install TypeScript globally on your computer using the command `npm install -g typescript`.*

While running the TypeScript code, you need to execute the above dependencies using a Node command. To do this, use a TypeScript execution engine and REPL library called [ts-node](https://typestrong.org/ts-node/). Ts-node allows you to run a one-time command that points to `.ts` files, compile and run them on the browser.

Go ahead and install ts-node like so:

```shell
npm install -D ts-node 
```

Then, edit your `package.json` script tags:

```json
"scripts": {
   "start": "ts-node ./src/index.ts"
},
```

This means ts-node will point the `/src/index.ts` file as the main file and execute the `.ts` code and modules that `index.ts` points to.

Finally, add a `@types/node` library to your project. Node runs on JavaScript, and this project uses TypeScript. Thus, you need to add type definitions for Node to run TypeScript.

[@types/node](https://www.npmjs.com/package/@types/node) contains built-in TypeScript definitions. To install it, run:

```shell
npm install @types/node
```

## How to create a simple TypeScript HTTP server

The TypeScript Node setup is ready to run your code. Let's see how to create a basic HTTP server that runs using the HTTP native module.

First, create an `src` folder and add an `index.ts` file. Then, set up a basic TypeScript HTTP server using Node with the following steps.

To begin, import the HTTP native module:

```shell
import HTTP from "HTTP";
```

To create an HTTP server and client, you need to use the HTTP command `from "http"`. This will allow you to have the necessary functions to create a server.

Next, create a local server from which to receive data:

```js
const myServer = http.createServer((req, res) => {
   res.write('Hello World!')
   res.end()
});
```

Set up a server instance using the `createServer()` function. This function allows you to issue HTTP requests and responses. The `res.write` code allows you to specify the incoming message that the server should execute. `res.end()` ends the set incoming requests even when no data is written to the body of the request or sent by the HTTP response.

Then, start the server and listen for connections:

```typescript
myServer.listen(3000, () => {
   console.log('Server is running on port 3000\. Go to http://localhost:3000/')
});

myServer.close()
```

`listen()` will create a localhost TCP server listening on port 3000\. In this case, `3000` must be the unused port that the server will be immediately get assigned to once it starts listening for connections. The `listen()` method is asynchronous, and manages how the server accepts new connections while exiting the current ones. When all connections have ended, the server is asynchronously closed. If an error occurs, the server will be called with an `Error` and closed immediately.

Once you run your app using `npm start`, the server will start, and you can access it on `http://localhost:3000/`. The `Hello World!` message specified in the response body will be served on your browser. This basic HTTP API is very low-level and runs on the most basic of TypeScript.

## How to create a CRUD TypeScript REST API

The above example only used the HTTP module to set a basic server. Let's dive in and build a REST API that uses the CRUD (create, read, update, and delete) methods.

To set up this API, start by storing our sample to-do list in a JSON file. Then, create a `store.json` file inside the `src` folder and add the following list of tasks:

```json
[
   {
     "id": 1,
     "title": "Learn React",
     "completed": false
   },
   {
     "id": 2,
     "title": "Learn Redux",
     "completed": false
   },
   {
     "id": 3,
     "title": "Learn React Router",
     "completed": false
   },
   {
     "id": 4,
     "title": "Cooking Lunch",
     "completed": true
   }
]
```

The to-do list API will refer to this data to perform server-based methods like GET, POST, PUT, and DELETE.

### Setting up the task structure

When using TypeScript, you can use classes, inheritance, interfaces, and other object-oriented forms. JavaScript uses classes, but these classes are templates for JavaScript objects.

JavaScript has no interfaces, because they are only available in TypeScript. Interfaces help you define types that keep you within the margins of your code. This ensures that parameters and variable structures are strongly typed.

Interfaces basically mirror the structure of an object that can be passed to classes as a parameter or implemented by a class. They define the structure and specify the types only once. Thus, you can reuse them anywhere in your code without having to duplicate the same types every time.

You can use interfaces to mirror the structure of the task data. This will specify the structure of the object you want your API to interact with. In this case, when you call the API, you get the task information with the same structure that mirrors the task data.

Let's use interfaces to define what properties the to-do list API should have. Create a new file inside the `src` directory and call it `ITask.ts`.

Inside this file, define the task structure as such:

```typescript
// Task structure
interface Task {
   id: number;
   title: string;
   completed: boolean;
}

export { Task }
```

This will create a model that defines the domain data. Ensure you export it so that other project modules can access its properties.

### Adding the API controllers

A controller is used to handle the HTTP requests that send back an HTTP response. The API controller function handles these requests for an endpoint. A controller regulates the structure defined in `ITask.ts` and the data that an API endpoint returns to the user. In this case, each controller will handle the logic handling each CRUD operation.

Go ahead and create a `controller.ts` file inside the `src` directory. Then, add the following imports and create each CRUD controller like so:

```typescript
// access the data store (store.json)
import fs from "fs";
import path from "path";

// handle requests and reponses
import { ServerResponse, IncomingMessage } from "http";

// access the task structure
import { Task } from "./ITask";
```

### Fetching tasks

Create a function `getTasks()`. This function fetches all the tasks listed in the `store.json` file:

```json
const getTasks = (req: IncomingMessage, res: ServerResponse) => {
   return fs.readFile(
     path.join(__dirname, "store.json"),
     "utf8",
     (err, data) => {
       // Read the store.json file
       // Check out any errors
       if (err) {
         // error, send an error message
         res.writeHead(500, { "Content-Type": "application/json" });
         res.end(
           JSON.stringify({
             success: false,
             error: err,
           })
         );
       } else {
         // no error, send the data
         res.writeHead(200, { "Content-Type": "application/json" });
         res.end(
           JSON.stringify({
             success: true,
             message: JSON.parse(data),
           })
         );
       }
     }
   );
};
```

The user sends a request using an endpoint that points to the `getTasks()` function. This controller will receive that request and what that request wants to do. Then, the `ITask` interface will set the data and give the response. The controller `getTasks()` will get this response and pass its data to the executed endpoint. In this case, the controller will read the data stored in the `store.json` file and return the to-do list.

### Adding a new a task

To begin, create a function called `addTask()`. This `addTask()` controller will handle the logic of adding a new task, like so:

```typescript
const addTask = (req: IncomingMessage, res: ServerResponse) => {
   // Read the data from the request
   let data = "";

   req.on("data", (chunk) => {
     data += chunk.toString();
   });

   // When the request is done
   req.on("end", () => {
     let task = JSON.parse(data);

     // Read the store.json file
     fs.readFile(path.join(__dirname, "store.json"), "utf8", (err, data) => {
       // Check out any errors
       if (err) {
         // error, send an error message
         res.writeHead(500, { "Content-Type": "application/json" });
         res.end(
           JSON.stringify({
             success: false,
             error: err,
           })
         );
       } else {
         // no error, get the current tasks
         let tasks: [Task] = JSON.parse(data);
         // get the id of the latest task
         let latest_id = tasks.reduce(
           (max = 0, task: Task) => (task.id > max ? task.id : max),
           0
         );
         // increment the id by 1
         task.id = latest_id + 1;
         // add the new task to the tasks array
         tasks.push(task);
         // write the new tasks array to the store.json file
         fs.writeFile(
           path.join(__dirname, "store.json"),
           JSON.stringify(tasks),
           (err) => {
             // Check out any errors
             if (err) {
               // error, send an error message
               res.writeHead(500, { "Content-Type": "application/json" });
               res.end(
                 JSON.stringify({
                   success: false,
                   error: err,
                 })
               );
             } else {
               // no error, send the data
               res.writeHead(200, { "Content-Type": "application/json" });
               res.end(
                 JSON.stringify({
                   success: true,
                   message: task,
                 })
               );
             }
           }
         );
       }
     });
   });
};
```

In the code above, a user sends a request using an endpoint that points to the `AddTasks()` function. This controller will first read the data from the request, which is adding a new task. Then, it reads the `store.json` file and prepares it to receive new data entries. The `ITask` interface will set the properties needed to create a new task and give the response to `AddTasks()`.

If the sent request matches the structure set by `ITask`, `AddTasks()` will accept its message and write the new task details to the `store.json` file.

### Updating a task

You might want to update the values of an existing task. This will require you to send a request to inform the save that you want to update some values.

To do so, create an `updateTask()` function like so:

```typescript
const updateTask = (req: IncomingMessage, res: ServerResponse) => {
   // Read the data from the request
   let data = "";
   req.on("data", (chunk) => {
     data += chunk.toString();
   });
   // When the request is done
   req.on("end", () => {
     // Parse the data
     let task: Task = JSON.parse(data);
     // Read the store.json file
     fs.readFile(path.join(__dirname, "store.json"), "utf8", (err, data) => {
       // Check out any errors
       if (err) {
         // error, send an error message
         res.writeHead(500, { "Content-Type": "application/json" });
         res.end(
           JSON.stringify({
             success: false,
             error: err,
           })
         );
       } else {
         // no error, get the current tasks
         let tasks: [Task] = JSON.parse(data);
         // find the task with the id
         let index = tasks.findIndex((t) => t.id == task.id);
         // replace the task with the new one
         tasks[index] = task;
         // write the new tasks array to the store.json file
         fs.writeFile(
           path.join(__dirname, "store.json"),
           JSON.stringify(tasks),
           (err) => {
             // Check out any errors
             if (err) {
               // error, send an error message
               res.writeHead(500, { "Content-Type": "application/json" });
               res.end(
                 JSON.stringify({
                   success: false,
                   error: err,
                 })
               );
             } else {
               // no error, send the data
               res.writeHead(200, { "Content-Type": "application/json" });
               res.end(
                 JSON.stringify({
                   success: true,
                   message: task,
                 })
               );
             }
           }
         );
       }
     });
   });
};
```

This will check the data sent against the existing data stored in the `store.json` file. In this case, the server will check if the ID value matches any existing tasks. `ITask` will check if the update values match the set structure, and return a response to `updateTask()`. If so, the value will be updated and a response sent to the requesting endpoint.

### Deleting a task

Likewise, you can delete a task from the storage. Here is the code to help you send a request that executes a delete request:

```typescript
const deleteTask = (req: IncomingMessage, res: ServerResponse) => {
   // Read the data from the request
   let data = "";
   req.on("data", (chunk) => {
     data += chunk.toString();
   });
   // When the request is done
   req.on("end", () => {
     // Parse the data
     let task: Task = JSON.parse(data);
     // Read the store.json file
     fs.readFile(path.join(__dirname, "store.json"), "utf8", (err, data) => {
       // Check out any errors
       if (err) {
         // error, send an error message
         res.writeHead(500, { "Content-Type": "application/json" });
         res.end(
           JSON.stringify({
             success: false,
             error: err,
           })
         );
       } else {
         // no error, get the current tasks
         let tasks: [Task] = JSON.parse(data);
         // find the task with the id
         let index = tasks.findIndex((t) => t.id == task.id);
         // remove the task
         tasks.splice(index, 1);
         // write the new tasks array to the store.json file
         fs.writeFile(
           path.join(__dirname, "store.json"),
           JSON.stringify(tasks),
           (err) => {
             // Check out any errors
             if (err) {
               // error, send an error message
               res.writeHead(500, { "Content-Type": "application/json" });
               res.end(
                 JSON.stringify({
                   success: false,
                   error: err,
                 })
               );
             } else {
               // no error, send the data
               res.writeHead(200, { "Content-Type": "application/json" });
               res.end(
                 JSON.stringify({
                   success: true,
                   message: task,
                 })
               );
             }
           }
         );
       }
     });
   });
};
```

Finally, export these controllers so that other application modules can access them:

```typescript
export { getTasks, addTask, updateTask, deleteTask };
```

### Setting the server and the task routes

Once your controllers are set, you need to create and expose various endpoints for creating, reading, updating, and deleting tasks. Endpoints are URLs that execute the requesting data.

This endpoint will be used in combination with an HTTP method to perform a specific action on the data. These HTTP methods include GET, POST, PUT, and DELETE. Each HTTP method will be mapped to a single controller that matches its defined routing.

Navigate to your `./src/index.ts` file and set your method endpoints as such:

```typescript
import HTTP from "HTTP";

// import controller
import { getTasks, addTask, updateTask, deleteTask } from "./controller";

// create the http server
const server = http.createServer((req, res) => {
   // get tasks
   if (req.method == "GET" && req.url == "/api/tasks") {
     return getTasks(req, res);
   }

   // Creating a task
   if (req.method == "POST" && req.url == "/api/tasks") {
     return addTask(req, res);
   }

   // Updating a task
   if (req.method == "PUT" && req.url == "/api/tasks") {
     return updateTask(req, res);
   }

   // Deleting a task
   if (req.method == "DELETE" && req.url == "/api/tasks") {
     return deleteTask(req, res);
   }
});

// set up the server port and listen for connections
server.listen(3000, () => {
   console.log("Server is running on port 3000");
});
```

This defines four endpoints:

*   Getting tasks, or GET `http://localhost:3000/api/tasks`
*   Creating task, or POST `http://localhost:3000/api/tasks`
*   Updating tasks, or PUT `http://localhost:3000/api/tasks)`
*   Deleting tasks, or DELETE `http://localhost:3000/api/tasks`

This will also expose this endpoint on the local host. The server will be mapped to port 3000.

Once it is up and running, it will listen for connections based on the execute routes. The to-do list API is ready, and you can run it using `npm start` and start testing different endpoints.

## Conclusion

Running your application with vanilla code gives you an idea of the code running the base of your apps. Vanilla TypeScript will help you create packages that other developers can use to speed up their development workflow.

The biggest drawback of any vanilla code is that you will be required to write many lines of code to execute an average task. The same task can still run using the packages that allow you to write a few lines of code. This means when running vanilla code, you will have to manage most operations within your application.

---

## Writing a lot of TypeScript? [Watch the recording](https://blog.logrocket.com/typescript-meetup-recap/?utm_source=social&utm_medium=organic&utm_campaign=21Q3_WB_TS-Tech-Meetup0930#utm_source=social&utm_medium=organic&utm_campaign=21Q3_WB_TS-Tech-Meetup0930) of our recent TypeScript meetup to learn about writing more readable code.

[![Write More Readable Code with TypeScript 4.4](https://blog.logrocket.com/wp-content/uploads/2021/07/typescript-4-4-more-readable-code.png)](https://blog.logrocket.com/typescript-meetup-recap/?utm_source=social&utm_medium=organic&utm_campaign=21Q3_WB_TS-Tech-Meetup0930#utm_source=social&utm_medium=organic&utm_campaign=21Q3_WB_TS-Tech-Meetup0930)

TypeScript brings type safety to JavaScript. There can be a tension between type safety and readable code. Watch the recording for a deep dive on some new features of TypeScript 4.4.