Adding a New Endpoint (Routes, Controllers, Services)

Using the existing Users table as an example, we will demonstrate how to add a POST endpoint /api/users for creating new users.

As already mentioned in the Getting Started section, the folder structure in the backend has the following structure:

  • backend: Root directory for the backend code.
    • src: Contains all source code files.
      • auth: Handles authentication logic (e.g., JWT handling, login, registration).
      • db: Defines database schema, api, seeders, migration, configs and utils.
      • middlewares: Contains middleware functions for Express.js (check-permissions, upload.js).
      • routes: Defines the API routes/endpoints that interact with services.
      • services: Contains business logic and service functions that interact with the database. Database api functions are located in src/db/api folder. The services also contain code responsible for sending messages and downloading/uploading files.
      • config.js: Configuration settings for the backend (gcloud config, email settings, host, ports, etc.).
    • index.js: The entry point for the backend server, typically initializes the server and sets up middleware and routes.
    • Dockerfile: Defines the Docker image for the backend, specifying the environment setup, dependencies, and how to run the backend service.
    • package.json: Lists dependencies, scripts, and metadata for the project.

Detailed Structure of the db Folder

In this guide, we will delve into the db folder and its structure.

  • db: This root directory contains all the source code files related to database operations.
    • api: This folder houses all the DB API files responsible for interacting with the database. These files define the CRUD operations and other queries necessary for database communication.
    • migrations: This directory contains migration files used to create or modify database entities. Each migration file includes up and down functions that describe how to apply or revert a particular schema change, ensuring the database remains consistent and up-to-date.
    • models: Within this folder, you will find files that describe the database entities. These model files use ORM (Object-Relational Mapping) to define the structure and relationships of the tables in the database. Each model corresponds to a table and includes attributes, data types, validations, and associations with other models.
    • seeders: This directory contains seed files, which are used to populate the database with initial or sample data. Each seeder file typically inserts data into one or more tables, ensuring the database starts with a known state.

Working with Migrations and the Database

For the Users entity, a migration will have already been created during the project's initialization.

The full code can be found in the backend/src/db/migrations folder.

Here is a part of the migration code:

async up(queryInterface, Sequelize) {
  /**
   * @type {Transaction}
   */
  const transaction = await queryInterface.sequelize.transaction();
  try {
    await queryInterface.createTable(
      'users',
      {
        id: {
          type: Sequelize.DataTypes.UUID,
          defaultValue: Sequelize.DataTypes.UUIDV4,
          primaryKey: true,
        },
        createdById: {
          type: Sequelize.DataTypes.UUID,
          references: {
            key: 'id',
            model: 'users',
          },
        },
        updatedById: {
          type: Sequelize.DataTypes.UUID,
          references: {
            key: 'id',
            model: 'users',
          },
        },
        createdAt: { type: Sequelize.DataTypes.DATE },
        updatedAt: { type: Sequelize.DataTypes.DATE },
        deletedAt: { type: Sequelize.DataTypes.DATE },
        importHash: {
          type: Sequelize.DataTypes.STRING(255),
          allowNull: true,
          unique: true,
        },
      },
      { transaction },
    );
...

Working with the Database API

Next, in the backend/src/db/api/users.js file, we define functions for interacting with the database and the Users table.

Below is a part of the create function for users:

...

static async create(data, options) {
  const currentUser = (options && options.currentUser) || { id: null };
  const transaction = (options && options.transaction) || undefined;

  const users = await db.users.create(
    {
      id: data.data.id || undefined,
      firstName: data.data.firstName || null,
      lastName: data.data.lastName || null,
      phoneNumber: data.data.phoneNumber || null,
      email: data.data.email || null,
      disabled: data.data.disabled || false,
...
}
...

Creating Services

Next, we create the create method in the UsersService class. The full code of the service can be found at backend/src/services/users.js.

const UsersDBApi = require('../db/api/users');

...

static async create(data, currentUser, sendInvitationEmails = true, host) {
  let transaction = await db.sequelize.transaction();

  let email = data.email;
  let emailsToInvite = [];
  try {
    if (email) {
      let user = await UsersDBApi.findBy({ email }, { transaction });
      if (user) {
        throw new ValidationError('iam.errors.userAlreadyExists');
      } else {
        await UsersDBApi.create(
          { data },
          {
            currentUser,
            transaction,
          },
        );
        emailsToInvite.push(email);
      }
    } else {
      throw new ValidationError('iam.errors.emailRequired');
    }
    await transaction.commit();
  } catch (error) {
    await transaction.rollback();
    throw error;
  }
  if (emailsToInvite && emailsToInvite.length) {
    if (!sendInvitationEmails) return;

    AuthService.sendPasswordResetEmail(email, 'invitation', host);
  }
}

...

Routing

Below is an example of a route for creating new users, which can be found in the backend/src/routes/users.js file.

const router = express.Router();
const UsersService = require('../services/users');

...

router.post(
  '/',
  wrapAsync(async (req, res) => {
    const link = new URL(req.headers.referer);
    await UsersService.create(req.body.data, req.currentUser, true, link.host);
    const payload = true;
    res.status(200).send(payload);
  }),
);
...

All routes are located in the backend/src/routes folder. Here, we can declare any necessary routes and handler functions.

Finally, in the backend/src/index.js file, we map the '/api/users' path to the usersRoutes.

const app = express();
const usersRoutes = require('./routes/users');

...

app.use(
  '/api/users',
  passport.authenticate('jwt', { session: false }),
  usersRoutes,
);
...

Now, you can send POST requests to the '/api/users' path with the necessary information to create a new user.