Skip to content

Getting Started with PRS Backend Development

This guide will help you set up your development environment and understand the basics of working with the PRS Backend codebase. For frontend development, see Getting Started with PRS Frontend Development.

Prerequisites

Before you begin, make sure you have the following installed:

  • Node.js (v14 or later)
  • npm (v6 or later)
  • PostgreSQL (v12 or later)
  • Git

Setting Up the Development Environment

1. Clone the Repository

Bash
git clone <repository-url>
cd prs-backend

2. Install Dependencies

Bash
npm install

3. Set Up Environment Variables

Copy the example environment file and update it with your local configuration:

Bash
cp .env.example .env

Edit the .env file with your database credentials and other configuration options.

4. Set Up the Database

Create a PostgreSQL database for the project:

Bash
createdb prs_development

Run the database migrations:

Bash
npm run db:migrate

Seed the database with initial data:

Bash
npm run db:seed

5. Start the Development Server

Bash
npm run dev

The server will start on http://localhost:3000 by default.

Project Structure

The PRS Backend follows a layered architecture pattern:

Text Only
src/
├── app/                  # Application layer
│   ├── errors/           # Error classes and handlers
│   ├── handlers/         # Controllers and middlewares
│   └── services/         # Business logic services
├── domain/               # Domain layer
│   ├── constants/        # Domain constants and enums
│   └── entities/         # Domain entities and validation schemas
├── infra/                # Infrastructure layer
│   ├── database/         # Database configuration and models
│   ├── logs/             # Logging configuration
│   └── repositories/     # Data access repositories
├── interfaces/           # Interface layer
│   └── router/           # API routes
└── utils/                # Utility functions

Key Concepts

1. Dependency Injection

The PRS Backend uses Awilix for dependency injection. Services and repositories are registered in the container and injected into controllers and services.

JavaScript
// src/container.js
const container = createContainer();

container.register({
  // Repositories
  requisitionRepository: asClass(RequisitionRepository),

  // Services
  requisitionService: asClass(RequisitionService),

  // Controllers
  requisitionController: asClass(RequisitionController),
});

2. Controllers

Controllers handle HTTP requests and delegate business logic to services:

JavaScript
// src/app/handlers/controllers/requisitionController.js
class RequisitionController {
  constructor({ requisitionService, db, utils, entities, constants, clientErrors }) {
    this.requisitionService = requisitionService;
    this.db = db;
    this.utils = utils;
    this.entities = entities;
    this.constants = constants;
    this.clientErrors = clientErrors;
  }

  async getAll(request, reply) {
    try {
      const requisitions = await this.requisitionService.getAll(request.query);
      return reply.status(200).send(requisitions);
    } catch (error) {
      // Error handling
    }
  }
}

3. Services

Services implement business logic and orchestrate operations:

JavaScript
// src/app/services/requisitionService.js
class RequisitionService extends BaseService {
  constructor({ requisitionRepository, db, constants, clientErrors }) {
    super({ repository: requisitionRepository });
    this.requisitionRepository = requisitionRepository;
    this.db = db;
    this.constants = constants;
    this.clientErrors = clientErrors;
  }

  async getAll(query) {
    // Business logic
    return this.requisitionRepository.findAll(query);
  }
}

4. Repositories

Repositories handle data access and persistence:

JavaScript
// src/infra/repositories/requisitionRepository.js
class RequisitionRepository extends BaseRepository {
  constructor({ db }) {
    super(db.requisitionModel);
    this.db = db;
  }

  async findAll(query) {
    // Data access logic
    return super.findAll(query);
  }
}

5. Domain Entities

Domain entities define validation schemas and basic business rules:

JavaScript
1
2
3
4
5
6
7
8
9
// src/domain/entities/requisitionEntity.js
const createRequisitionSchema = z
  .object({
    isDraft: z.enum(['true']).transform(value => value === 'true'),
    type: z.enum(REQUISITION_REQUEST_TYPES),
    companyId: createNumberSchema('Company ID'),
    // ... other fields
  })
  .strict();

Common Development Tasks

1. Creating a New API Endpoint

To create a new API endpoint, you need to:

  1. Create or update a controller method
  2. Create or update a service method
  3. Create or update a repository method (if needed)
  4. Add a route in the appropriate router file

Example:

JavaScript
// 1. Controller method
async getRequisitionsByStatus(request, reply) {
  try {
    const { status } = request.params;
    const requisitions = await this.requisitionService.getByStatus(status);
    return reply.status(200).send(requisitions);
  } catch (error) {
    // Error handling
  }
}

// 2. Service method
async getByStatus(status) {
  return this.requisitionRepository.findAll({
    where: { status },
  });
}

// 3. Route
fastify.route({
  method: 'GET',
  url: '/status/:status',
  handler: requisitionController.getRequisitionsByStatus.bind(requisitionController),
});

2. Adding Validation

To add validation to an API endpoint:

  1. Create a validation schema in the appropriate entity file
  2. Add the schema to the route definition

Example:

JavaScript
// 1. Validation schema
const getRequisitionByStatusSchema = z
  .object({
    status: z.enum(Object.values(REQUISITION_STATUS)),
  })
  .strict();

// 2. Route with validation
fastify.route({
  method: 'GET',
  url: '/status/:status',
  schema: {
    params: getRequisitionByStatusSchema,
  },
  handler: requisitionController.getRequisitionsByStatus.bind(requisitionController),
});

3. Handling Transactions

To handle transactions:

JavaScript
async createRequisition(request, reply) {
  const transaction = await this.db.sequelize.transaction();

  try {
    const requisition = await this.requisitionService.create(request.body, transaction);
    await transaction.commit();
    return reply.status(201).send(requisition);
  } catch (error) {
    await transaction.rollback();
    // Error handling
  }
}

Debugging

1. Logging

The PRS Backend uses Pino for logging. You can add logs to your code:

JavaScript
request.log.info({ data }, 'Message');
request.log.error({ error }, 'Error message');

2. Debugging with VS Code

You can debug the application using VS Code's built-in debugger. Create a .vscode/launch.json file:

JSON
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "skipFiles": ["<node_internals>/**"],
      "program": "${workspaceFolder}/index.js",
      "env": {
        "NODE_ENV": "development"
      }
    }
  ]
}

Additional Resources