API development with nodejs, express and typescript from scratch — Basic API

This is the third part of the series “API development with nodejs, express and typescript from scratch” and it is all about starting to build our API with the advanced project starter.
We will cover the following steps:
- First Route
- Routers
- Controllers
- Data Integration
We will continue using our advanced project starter that we created in the previous part of the series. If you have not seen the previous part, follow the link below and start by creating the advanced project starter from scratch or clone the individual parts of the series from the github repository and use npm install to get started.
Make sure to set everything up and without further ado, let’s get started.
First Route
In this section we will create our first API endpoint or in other terms route.
A “route” is a mapping between a URL and the corresponding handling function that is executed when a user requests that URL. A route is essentially a way to handle HTTP requests in your Express application and acts as an endpoint.
Each route is defined with a specific URL endpoint and an associated handling function, which is responsible for sending a response to the user. The handling function can return dynamic data, read data from a database, or render a view template, among other things.
Actually we already created our first route in our index.ts file during the starter tutorials:
app.get('/', (req, res) => {
res.send('Hello World!');
});First of all, let’s open our advanced express starter in your code editor. Let’s have a look at our application.
When our application is running and we type its url into our browser, which is localhost:3000 in our case, we are essentially making an http request to our application running on ‘localhost:3000’ at the route ‘/’ with the request method ‘GET’.
As you can see in the code above, when our application receives a ‘GET’ request at the route ‘/’ it will send the response ‘Hello World!’. And this is exactly what we are seeing in our browser.
Now let’s add another route in our index.ts file to our application:
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.get('/api', (req, res) => {
res.send('This is an API endpoint!');
});Save the file, then copy and paste the following into your browser:
localhost:3000/apiCongratulations, you have mastered the art of creating an API.
On the one hand it is nice that we can create our own api endpoints now, but on the other hand we cannot do much with it. We need some CRUD functionality, so we can build something that we can interact with and not only get responses with some static text. But before we are going to do that, we are going to have a look at how we can structure our project so that we do not get lost when our application gets bigger.
Routers
Routers can help us to structure our project so that we can maintain our code when our project is getting bigger and bigger.
Let’s go ahead and make use of them by creating a new folder in our src directory called routers and add a file to it called routes.ts:
src/routers/routes.tsYou can name your routes as you want. A common way to structure your routes is by grouping together related functionality. For example if you are creating a social media app, you can create a users.routes.ts and a posts.routes.ts file to handle the requests depending on what you want to get, post, update or delete. As this part is about creating a basic api, we will keep it simple for now and only create one file.
As we are going to use the Router, go ahead and delete the routes from the index.ts file:
DELETE the following:
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.get('/api', (req, res) => {
res.send('This is an API endpoint!');
});Now let’s go to the routes.ts file that we created and recreate the routes that we just deleted, but this time we will use the express.Router class to create modular, mountable route handlers.
Copy the following code to your routes.ts file:
import { Router, Request, Response } from 'express';
const router = Router();
router.get('/', async (req: Request, res: Response) => {
try {
res.status(200).send('Hello World!');
} catch (e) {
res.status(500).send(e.message);
}
});
router.get('/api', async (req: Request, res: Response) => {
try {
res.status(200).send('This is an API endpoint!');
} catch (e) {
res.status(500).send(e.message);
}
});
router.get('/api/:id', async (req: Request, res: Response) => {
try {
res.status(200).json({ id: req.params.id });
} catch (e) {
res.status(500).send(e.message);
}
});
export default router;As you can see in the code above, we added some additional things to our basic routes. In addition to the express.Router we also added async functionality, which will be useful when connecting to a database later on, and a try-catch block including an http status response message.
We also imported the Request and Response type definition from express. This helps us making sure that the req and res property can only be of type express.Request and type express.Response respectively.
Moreover we added a new route, to see how we can access the information of an http request and respond with a json message. Before we can test our routes we have to import them in our index.ts file and make sure that our app is using them with app.use() as shown in the following:
import router from '@/routers/routes';
// Routes
app.use('/', router);The app will now be able to handle requests to '/', '/api' and '/api/{some id}'.
However, the first parameter that we pass to app.use() lets us define a base route. This means, that if we change it from '/' to '/route', the app will only be able to handle requests to '/route', '/route/api' and '/route/api/{some id}'. We are not going to change this and keep using '/'.
Now we can test our API. Go ahead and try it in your browser by using the following requests:
localhost:3000/Response: Hello World!
localhost:3000/apiResponse: This is an API endpoint!
localhost:3000/api/1Response: {“id”:”1"}
At this point we have created some basic API endpoints using the express.Router class. We have started to give our project some structure by creating a separate directory for our routes, however we can improve our structure further by creating something called controller. More on that in the next section.
Controllers
A controller, in this context, is nothing more than a function that contains some logic that we write and that we organize in a separate directory that we then call controllers.
Or in more accurate terms, a controller is a module that contains the logic for handling a specific aspect of the application. Controllers are used to process incoming requests, interact with other parts of your application, such as the database or service layer, and return a response to the client. In general, a controller acts as an intermediary between your Express application and the various components that make up your application.
We use controllers to organize the application logic, which makes it easier to maintain and test later on. By separating the application logic into different controllers, you can keep your code clean, organized, and easy to understand.
In TypeScript, you can define a controller as a class, with each class method representing a different route handling function. This allows you to take advantage of TypeScript’s object-oriented features, such as inheritance, encapsulation, and polymorphism, to structure your application logic. And this is exactly what we are going to do next.
Let’s create a new directory called controllers and a file called controller.ts:
src/controllers/controller.tsKeep in mind that we are creating a very basic API at this point, that is why we name our routes route and our controllers controller, but as your application grows you want to create different files and group the controllers by functionality just like our routes.
First lets import the following into our controller.ts file:
import { Request, Response } from 'express';Then we create our controller by creating a class called Controller and we add the logic of our routes, that we created before, as methods to it:
class Controller {
public getIndex = async (req: Request, res: Response) => {
try {
res.status(200).send('Hello World!');
} catch (e) {
res.status(500).send(e.message);
}
};
public getApi = async (req: Request, res: Response) => {
try {
res.status(200).send('This is an API endpoint!');
} catch (e) {
res.status(500).send(e.message);
}
};
public getApibyId = async (req: Request, res: Response) => {
try {
res.status(200).json({ id: req.params.id });
} catch (e) {
res.status(500).send(e.message);
}
};
}
export default Controller;We can now use import Controller from '@controllers/controller' in our route.ts file, create a new instance of our defined object type and call the corresponding method for each route as shown in the following. We can also delete Request and Response from our imports as we now have them in our controller file. Go ahead and edit your route.ts file:
import { Router } from 'express';
import Controller from '@controllers/controller';
const router = Router();
const controller = new Controller();
router.get('/', controller.getIndex);
router.get('/api', controller.getApi);
router.get('/api/:id', controller.getApibyId);
export default router;You can test your API again. We have now successfully separated the logic to handle requests and responses from our routes and further improved the structure of our app. It is much easier now to get an overview of our routes and by using strg+leftclick or cmd+leftclick on the specific controller method that we want to check and it takes us directly to this method.
Now that we have set up routing and we are able to handle http requests we can move on an start with the fun part. Let’s add some data.
Data Integration
Quick disclaimer at this point, we will not add a database in this tutorial. We will have a look at databases in the next part of this series. It is a good idea to start simple.
Let’s create some mock data. Go ahead and create a new file in our utils directory called data.ts:
src/utils/data.tsAdd the following code to it:
const users: { name: string, email: string, id: number }[] = [
{ name: "John Doe", email: "[email protected]", id: 1 },
{ name: "Jane Doe", email: "[email protected]", id: 2 },
{ name: "Jim Smith", email: "[email protected]", id: 3 }
];This is an array of objects, where each object represents a user. The objects have properties for the name, email, and id of each user. The
{ name: string, email: string, id: number }[]syntax is used to specify the type of the array elements, which are objects with properties of typestring,string, andnumberforname,idrespectively.
However, the best practice would be to create a new map, but, in my view, this is easier to understand in the beginning.
To be able to make a request to get all user and a user by id, we have to create a new controller and new routes. Let’s try to evolve our project structure and organize everything related to users in a new controller and routes file.
First let’s create a new controller file called users.controller.ts and add the controller:
import { Request, Response } from 'express';
import users from '@/utils/data';
class UsersController {
public getUsers = async (req: Request, res: Response) => {
try {
res.status(200).json(users);
} catch (e) {
res.status(500).send(e.message);
}
};
public getUserById = async (req: Request, res: Response) => {
try {
const user = users.find(u => u.id === parseInt(req.params.id));
if (user) {
res.status(200).json(user);
} else {
res.status(404).send('User not found');
}
} catch (e) {
res.status(500).send(e.message);
}
};
}
export default UsersController;Then create the new routes file called users.routes.ts and the new routes:
import { Router } from 'express';
import UsersController from '@controllers/users.controller';
const usersRouter = Router();
const usersController = new UsersController();
usersRouter.get('/users', usersController.getUsers);
usersRouter.get('/users/:id', usersController.getUserById);
export default usersRouter;Do not forget to add the new usersRouter to our index.ts file:
.
.
.
import usersRouter from './routers/users.routes';
.
.
.
// Routes
app.use('/', router);
app.use('/', usersRouter);
.
.
.At this point, our src directory looks like this:
src/
controllers/
controller.ts
users.controller.ts
routers/
routes.ts
users.routes.ts
utils/
data.ts
validateEnv.ts
index.tsFinally, run the dev script and test the API:
localhost:3000/userslocalhost:3000/users/1If everything worked, we have successfully set the basis for a well structured API with express. We will continue with the development in the next part of the series.
Finish
We have successfully developed a basic API, discussed Routers and Controllers, and we used them to organize our project.
In the next part of this series, we will continue building our production ready API by integrating a database and develop some CRUD functionality. We will make use of Docker Compose, MongoDB, Prisma and Postman.
Content of the next part:
- Database Setup using Docker Compose and MongoDB
- Prisma as ORM tool
- Services
- Testing our API with Postman
Go ahead and check out the next part of this series:
If you find this tutorial helpful, feel free to clap and follow me to stay up to date with all the articles from myself. More tutorials and insights are on the way.
If you want to get unlimited, ad-free access to all the stories on Medium, you can subscribe by using my referral link below :)
