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. 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 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.
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 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:
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 commandnpm 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. 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:
npm install -D ts-node
Then, edit your package.json
script tags:
"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 contains built-in TypeScript definitions. To install it, run:
npm install @types/node
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:
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:
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:
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.
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:
[ { "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.
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:
// 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.
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:
// 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";
Create a function getTasks()
. This function fetches all the tasks listed in the store.json
file:
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.
To begin, create a function called addTask()
. This addTask()
controller will handle the logic of adding a new task, like so:
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.
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:
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.
Likewise, you can delete a task from the storage. Here is the code to help you send a request that executes a delete request:
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:
export { getTasks, addTask, updateTask, deleteTask };
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:
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:
http://localhost:3000/api/tasks
http://localhost:3000/api/tasks
http://localhost:3000/api/tasks)
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.
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.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowUse CSS to style and manage disclosure widgets, which are the HTML `details` and `summary` elements.
React Native’s New Architecture offers significant performance advantages. In this article, you’ll explore synchronous and asynchronous rendering in React Native through practical use cases.
Build scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
One Reply to "How to build a REST API with TypeScript using only native modules"
It should be noted that your first code snipped for importing HTTP doesn’t work as written. The library is specifically “http” (must be lowercase) and because you’re using “http” as the reference to the library the import should actually be:
import http from “http”;
in order for your code to work properly. Other than that, great post!