Building Your Own RESTful API: A Guide

A hands-on tutorial on how to build your own RESTful API with OpenAPI in Node.js

Geertjan Wielenga
Mar 9th, 2022
Share

Application programming interfaces (APIs) are software intermediaries that allow two or more applications to talk to each other and share resources, so one computer program can enable another’s capabilities. They’re how various programs communicate through a well-documented interface.

Well-defined and tested APIs underpin successful and scalable applications. They provide an interface for applications to communicate amongst themselves and encourage service reusability. They can also serve as a revenue generation scheme by building an API economy enterprise.

Meanwhile, representational state transfer (REST) is an architectural style for distributed hypermedia systems, which Roy Fielding first presented in his famous dissertation in the year 2000. Like other software architectural styles, REST defines six architectural constraints that a genuinely RESTful API must obey. These architectural guidelines include:

  • Uniform interface
  • Client-server separation
  • Stateless
  • Cacheable
  • Layered system
  • Code on demand (optional)

A service interface must satisfy these constraints to be RESTful. So, a web API (or web service) conforming to the REST architectural style is a REST API. Let’s explore OpenAPI, then demonstrate its specifications by building a simple Node.js application that satisfies RESTful API specifications.

What is OpenAPI?

The OpenAPI specification is an open specification within the OpenAPI Initiative. The specification works with any programming language’s interface description for HTTP APIs. OpenAPI enables humans and computers to find and understand a service’s capabilities without requiring access to source code, additional documentation, or network traffic examination.

When we properly define a remote service, consumers can understand and interact with it using minimal implementation logic. The OpenAPI specification removes uncertainty when calling a service, similar to what interface descriptions did for lower-level programming.

This post demonstrates the concept of the OpenAPI specifications using a simple Node.js application. Below, we’ll integrate Swagger and write out the documentation following the OpenAPI specification.

This tutorial requires installing Node.js and MongoDB and a basic understanding of Node.js and Typescript.

Setting up the project

First, set up the application’s folder structure. The complete working code for this application is on GitHub.

Building out the directory structure

Create a folder named EventsAppOpenAPI and open it in an editor. Next, make a basic Node.js application with the npm init command. Finally, build out the following folder structure:

EventsAppOpenAPI

  • src
    • controllers
      • event.controller.ts
    • docs
      • apidocs.ts
      • events.ts
    • models
      • event.model.ts
    • routes
      • event.route.ts
    • database.connection.ts
    • index.ts
  • .env
  • nodemon.json
  • README.md
  • tsconfig.json

Installing packages

Next, install the following packages using the npm install command on the CLI, as this code snippet shows:

#bash
$ npm install express mongoose cors dotenv swagger-ui-express

Also install these type packages as dev dependencies using the following command:

#bash
$ npm @types/express @types/mongoose @types/node @types/swagger-ui-express nodemon ts-node typescript --save-dev

Let’s go over the packages you installed in the first code snippet:

  • Express for your application server
  • Mongoose, a database tool providing a straightforward, schema-based solution to model application data
  • Cors for cross-site requests in the application
  • Dotenv for your environmental variables
  • Swagger-ui-express for OpenAPI documentation

Because you’re using TypeScript for this application, you install the typed version of the packages that were already installed from the first code snippet in the second code snippet.

You installed TypeScript Nodemon to restart the application when there are changes, ts-node, and a typed version of Node.

Note that you install them as dev dependencies, as you only need them for development and not for production.

After installing all packages, the content of the package.json file should be like this:

#package.json
{
 "name": "events-tracker-openapi",
 "version": "1.0.0",
 "description": "A simple Nodejs application illustrating the use of Open API",
 "main": "build/index.js",
 "scripts": {
 "start": "nodemon --config nodemon.json src/index.ts",
 "eslint:fix": "eslint --fix"
 },
 "author": "",
 "license": "ISC",
 "devDependencies": {
 "@types/express": "^4.17.13",
 "@types/mongoose": "^5.11.97",
 "@types/node": "^16.11.13",
 "@types/swagger-ui-express": "^4.1.3",
 "nodemon": "^2.0.15",
 "ts-node": "^10.4.0",
 "typescript": "^4.5.4"
 },
 "dependencies": {
 "dotenv": "^10.0.0",
 "express": "^4.17.1",
 "mongoose": "^6.1.1",
 "swagger-ui-express": "^4.2.0"
 }
}

This code snippet shows the contents of the package.json file after installing the dependencies and devDependencies required to kickstart the application. In the scripts tag, you configured the application’s runtime parameters. You also configured Nodemon to watch over the application files and restart the application whenever there is a change in the application files.

Next, put the Nodemon configurations in the nodemon.json file, as the code snippet below shows:

#nodemon.json
{
 "restartable": "rs",
 "ignore": [".git", "node_modules/", "build/", "coverage/"],
 "watch": ["src/"],
 "execMap": {
 "ts": "node -r ts-node/register"
 },
 "env": {
 "NODE_ENV": "development"
 },
 "ext": "js,json,ts"
 }

Having configured Nodemon, configure TypeScript before building out the application endpoints. To configure TypeScript in your app, paste the code snippet below into the tsconfig.json file:

#tsconfig.json
{
 "compilerOptions": {
 "target": "es5",
 "module": "commonjs",
 "lib": ["dom"],
 "outDir": "./build",
 "strict": true,
 "noImplicitAny": false,
 "strictPropertyInitialization": false,
 "baseUrl": "./",
 "esModuleInterop": true,
 "experimentalDecorators": true,
 "emitDecoratorMetadata": true
 },
 "include": [
 "./src/**/*",
 ],
 "exclude": [
 "node_modules"
 ]
}

The above code snippet configured how your typescript codes will transpile into JavaScript during the build. The code also identified the folders to transpile in the include tag and the folders to ignore in the exclude tag.

Now that you’re done the basic setup, let’s analyze the application you intend to build and the necessary endpoints needed to make that possible. The sample application helps manage and track important events. To achieve this, you need the following endpoints:

  • [POST] /events — to create events
  • [GET] /events — to retrieve all the events created
  • [GET] /events/:id— to fetch a single event, using the event ID
  • [PUT] /events/:id — to edit an event, using the event ID
  • [DELETE] /events/:id — to delete an event

Setting up the application server

To set up the application server, create an index.ts file with the following code and save it in the src folder located at the project’s root:

#index.ts

import express from 'express';
import dotenv from 'dotenv';
import swaggerUi from 'swagger-ui-express';

import { connectToDatabase } from './database.connection;
import { eventRoute } from './routes/event.route';
import { apiDocumentation } from './docs/apidoc';

dotenv.config();

const HOST = process.env.HOST || 'http://localhost';
const PORT = parseInt(process.env.PORT || '4500');

const app = express();

app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.use('/', eventRoute());
app.use('/docs', swaggerUi.serve, swaggerUi.setup(apiDocumentation));

app.get('/', (req, res) => {
 return res.json({ message: 'Hello Nodejs using Open API Specifications!' });
});

app.listen(PORT, async () => {
 await connectToDatabase();
 console.log(`Application started on URL ${HOST}:${PORT} 🎉`);
});

This code snippet specified express as your application server, dotenv for environmental variables, and swaggerUi for the endpoint documentation. Next, the code imported the application database configurations and routes, then configured the app to use the imported packages. Finally, the code configured the application to listen to a defined port.

Having already imported the database configurations file in the index.ts file, now create a database config file called database.connection.ts inside the src folder with this code snippet:

#database.connection.ts

const mongoose = require('mongoose');
import dotenv from 'dotenv';

mongoose.Promise = global.Promise;
dotenv.config();

const {DBURI} = process.env;

const connectToDatabase = async (): Promise<void> => {
 const options = { useNewUrlParser: true, useUnifiedTopology: true };
 await mongoose.connect(DBURI, options);
};

export { connectToDatabase };

The above code snippet imported dotenv and mongoose, your database tool. Then the code configured the application to connect to the database using the credentials in the .env file.

Making models

We’ve described how the event management application will look. Now, create an event model to make the description achievable.

First, create a models folder in the src directory as the application directory structure above illustrates, then create an event.model.ts file and paste in this code:

#src/models/events.model.ts
import mongoose, { Schema, Model, Document } from 'mongoose';

type EventInput = {
 name: EventDocument['name'];
 description: EventDocument['description'];
};

type EventDocument = Document & {
 name: string;
 description: string | null;
 status: string;
};

const eventSchema = new Schema(
 {
 name: {
 type: Schema.Types.String,
 required: true,
 unique: true,
 },
 description: {
 type: Schema.Types.String,
 default: null,
 },
 status: {
 type: Schema.Types.String,
 enum: ['Pending','Ongoing','Done'],
 default: 'Pending',
 },
 },
 {
 collection: 'events',
 timestamps: true,
 },
);

const Event: Model<EventDocument> = mongoose.model<EventDocument>('Event', eventSchema);

export { Event, EventInput, EventDocument };

The above code snippets define the mongoose database schema for an event with the necessary properties. The code also defines the custom type EventInput, which you use for input validation later in the controller. Then, the code defines EventDocument to extend the Mongoose document, which tells TypeScript about the schema object. You’ll later export these defined objects to use them in other parts of the application.

Building controllers

In the src folder, next create a folder called controllers, then the event.controller.ts file. Add the following code snippet to set up the application logic implementing the event management described earlier:

#src/controllers/events.controller.ts
import { Request, Response } from 'express';
import { Event, EventInput } from '../models/event.model';

const createEvent = async (req: Request, res: Response) => {
 const { description, name } = req.body;

 if (!name || !description) {
 return res.status(422).json({ message: 'The fields name and description are required' });
 }

 const EventInput: EventInput = {
 name,
 description,
 };

 const EventCreated = await Event.create(EventInput);

 return res.status(201).json({ data: EventCreated });
};


const getAllEvents = async (req: Request, res: Response) => {
 const events = await Event.find().sort('-createdAt').exec();

 return res.status(200).json({ data: events });
};

const getEvent = async (req: Request, res: Response) => {
 const { id } = req.params;

 const event = await Event.findOne({ _id: id });

 if (!event) {
 return res.status(404).json({ message: `Event with id "${id}" not found.` });
 }

 return res.status(200).json({ data: Event });
};


const updateEvent = async (req: Request, res: Response) => {
 const { id } = req.params;
 const { description, name } = req.body;

 const event = await Event.findOne({ _id: id });

 if (!event) {
 return res.status(404).json({ message: `Event with id "${id}" not found.` });
 }

 if (!name || !description) {
 return res.status(422).json({ message: 'The fields name and description are required' });
 }

 await Event.updateOne({ _id: id }, { name, description });

 const eventUpdated = await Event.findById(id, { name, description });

 return res.status(200).json({ data: eventUpdated });
};


const deleteEvent = async (req: Request, res: Response) => {
 const { id } = req.params;

 await Event.findByIdAndDelete(id);

 return res.status(200).json({ message: 'Event deleted successfully.' });
};

export { createEvent, getAllEvents, getEvent, updateEvent, deleteEvent };

In the event.controller.ts file code snippet, you created five functions:

  • createEvent
  • getAllEvents
  • getEvent
  • updateEvent
  • deleteEvent

The createEvent function accepts a POST request with payloads required to create an event. It processes the payloads, validates them, and creates an event using the request payloads provided.

The getAllEvents and getEvent functions accept GET requests. The getAllEvents function returns a list of all the events you have created so far in a JSON format, while the getEvent function returns complete details of each event using the eventId also in a JSON format.

In the next section, you’ll create endpoints to access the controllers’ functions and subsequently documentation for the endpoints.

Creating routes

To create the application routes, first create a routes folder in the src folder following the application directory pattern. Then, create an event.route.ts file and paste in this code snippet:

#src/routes/events.routes.ts
import { Router } from 'express';
import { createEvent, deleteEvent, getAllEvents, getEvent, updateEvent } from '../controllers/event.controller';

const eventRoute = () => {
 const router = Router();

 router.post('/events', createEvent);

 router.get('/events', getAllEvents);

 router.get('/events/:id', getEvent);

 router.put('/events/:id', updateEvent);

 router.delete('/events/:id', deleteEvent);

 return router;
};

export { eventRoute };

The code snippet defines the application routes following the OpenAPI standard described earlier. The code also exports the router and uses it in the index.ts file to be accessible.

Documenting the API

After implementing the application routes, models, and controllers, it’s time to document your endpoints. We use Swagger for this as we have the swagger-ui package already installed. You can use Swagger to document endpoints in two ways: YAML and JSON.

Documenting endpoints with YAML format

Documenting an API using this approach entails using multi-line comments to enclose your YAML formatted documentation right on top of the endpoint, like this:

/**
 * @swagger
 * components:
 * schemas:
 * Event:
 * type: objecti
 * required:
 * - name
 * - description
 * properties:
 * id:
 * type: integer
 * description: The auto-generated id for an event.
 * name:
 * type: string
 * description: The name of the event.
 * description:
 * type: string
 * description: A detailed description of an event.
 */
router.post('/events, createEvent);

Documenting endpoints with JSON format

You can also document API definitions as JSON objects following the JSON schema standard. View an example in the src/docs/events.ts file.

Testing the API

You’ve implemented a task management solution following OpenAPI specifications. Now, test your endpoints since you have already installed all the required packages.

So, start the application from the terminal using this command:

#bash
$ npm run start

If you did everything correctly, the application is live on http://localhost:5000, as the screenshot below shows.

To test the docs you specified, open a browser and navigate to localhost:5000/docs. You should see a colorful page (like the screenshot below) detailing all the application’s endpoints. Click on any tab to ensure the endpoints return what you expect.

Next steps

You now know how to create a RESTful Node.js application with OpenAPI endpoints. A REST API is helpful for providing clients with a consistent data structure and makes caching straightforward. This well-established type of API also tends to have plenty of support for integrating with third-party tools.

However, you can also explore other forms of API development, such as GraphQL. The GraphQL approach enables your users to decide how to access data and self-documenting. You may find that an alternative API to REST is better for your specific use.

Transposit’s connected workflow platform for DevOps enables you to integrate with anything through our API-first architecture and 200+ pre-built actions and connectors. Curious about how Transposit improves visibility, context, and actionability across people, processes, and APIs? See our documentation and request a demo.

Share