part-5.jpg

Learn Sails.js - Part 5

🛈

Excellent. The ship is seaworthy, the holds are filling with data, and you know how to steer. But a complex voyage requires more than just a single captain on the bridge shouting orders. It requires a specialist crew. It’s time to hire some experts.


Part 5: Assembling a Specialist Crew (Services)

  • Current Rank: Navigator
  • Objective: To learn how to organize and reuse your core application logic by creating Services, leading to cleaner, more maintainable, and more powerful code.

The Captain’s Briefing (Background and Theory)

Navigator, as your voyages become more ambitious, your Captain’s Log (the CaptainController.js file) will start to get cluttered. Imagine you need to perform a complex calculation, like determining the total value of a captain’s treasure, adjusted for inflation, tides, and the current price of rum.

Where would you put that code?

You could write a huge, messy function directly inside a controller action. But what if you need to perform that same exact calculation somewhere else, like in a different controller or in a script? You’d have to copy and paste the code. This is a cardinal sin in programming, a violation of the DRY (Don’t Repeat Yourself) principle. Copied code is twice as hard to maintain and twice as likely to have bugs.

This is the problem Services are designed to solve.

Services: The Specialist Crew

A Service is a dedicated specialist on your ship. It’s a file that lives in api/services/ and contains a collection of related helper functions. Think of them by their specialty:

  • EmailService.js: The ship’s messenger, responsible for sending all types of emails.
  • PaymentService.js: The ship’s purser, who knows how to handle all monetary transactions.
  • ImageManipulationService.js: The ship’s artist, skilled in resizing and watermarking images.
  • GeocodingService.js: Your master cartographer, who can turn a location name into latitude and longitude.

A controller’s job is to be the manager. It should be lean and decisive. It receives a request, calls on the appropriate specialists (models and services) to do the heavy lifting, and then decides what response to send. The controller gives the order; the service executes the skill.

Using Services

Once you create a service file like AppraisalService.js, Sails makes it globally available throughout your backend code. You don’t need to require() it. You can simply call its methods from any controller, policy, or even another service by using its global name.

If you have a function called calculateTax in AppraisalService.js, you can call it from anywhere like this: const taxedValue = await AppraisalService.calculateTax(treasure);

This makes your controllers incredibly clean and easy to read. Compare these two styles:

Bloated Controller (Bad):

// In a controller action
const treasure = await Treasure.findOne({ id: req.param('id') });
let taxedValue = treasure.value;
// ... 20 lines of complex tax calculation logic here ...
// ... what if the tax rules change? You have to find this code and change it.
return res.json({ taxedValue: taxedValue });

Clean Controller with Service (Good):

// In a controller action
const treasure = await Treasure.findOne({ id: req.param('id') });
// Call the specialist crew member!
const taxedValue = await AppraisalService.calculateTax(treasure);
return res.json({ taxedValue: taxedValue });

The logic is now neatly encapsulated in one place (AppraisalService.js), ready to be reused and easy to update.


Key Concepts Checklist

  • Service: A reusable module of helper functions for your core logic (api/services/).
  • DRY Principle: “Don’t Repeat Yourself.” The fundamental concept of not duplicating code.
  • Encapsulation: The practice of bundling related logic into a single, self-contained unit.
  • Business Logic: The core rules and processes that are unique to your application’s domain (e.g., how you calculate value, process an order, etc.). This is what belongs in services.
  • sails.helpers vs sails.services: While similar, services are traditionally seen as higher-level, stateless “buckets” of functions, while helpers are for smaller, granular tasks. For our purposes, services are the perfect place for our core “business logic.”

Mission Log: Quest - “Hire an Appraiser”

A new tax has been levied on all discovered treasure. We need to hire an Appraiser to calculate the value of a treasure after this new “Crown’s Tax” is applied.

  • Task 1: Recruit the Specialist (Create the Service File)

    1. This is one of the few things there isn’t a generator for. We create it manually.
    2. In your code editor, create a new folder named services inside the api/ directory.
    3. Inside api/services/, create a new file named AppraisalService.js.
  • Task 2: Define the Specialist’s Skill

    1. Open api/services/AppraisalService.js and add the following code. We’ll define a simple service with one method.

      // api/services/AppraisalService.js
      module.exports = {
      
        /**
         * Calculates the value of a treasure after applying the Crown's Tax.
         * @param {number} treasureValue The original value of the treasure.
         * @returns {number} The value after a 15% tax.
         */
        getTaxedValue: function(treasureValue) {
          const CROWN_TAX_RATE = 0.15; // 15%
          const taxAmount = treasureValue * CROWN_TAX_RATE;
          const finalValue = treasureValue - taxAmount;
      
          // We use Math.round to keep the value clean.
          return Math.round(finalValue);
        }
      
      };
      
  • Task 3: Create a Controller to Use the Service

    1. We need an endpoint to test this. Let’s add a new action to our CaptainController.js. This action will find a treasure and then use our new service to appraise it.
    2. Open api/controllers/CaptainController.js and add this new action.

      module.exports = {
        // ... your existing greet and logbook actions ...
      
        // Add this new action
        appraiseTreasure: async function(req, res) {
          try {
            // Find the first treasure we have in the database.
            const treasure = await Treasure.findOne();
      
            if (!treasure) {
              return res.status(404).json({ error: 'No treasure found to appraise.' });
            }
      
            // Here's the magic! Call the service.
            const taxedValue = AppraisalService.getTaxedValue(treasure.value);
      
            return res.json({
              treasureName: treasure.name,
              originalValue: treasure.value,
              taxedValue: taxedValue,
              message: `After the Crown's 15% tax, the value is ${taxedValue} doubloons.`
            });
      
          } catch (err) {
            return res.serverError(err);
          }
        }
      };
      

      Notice the use of async and await. This is modern JavaScript for handling asynchronous operations, like fetching from a database. It makes the code much cleaner.

  • Task 4: Chart the New Route

    1. Open config/routes.js and add a route for our new action.

      // ... previous routes
      'GET /logbook': 'CaptainController.logbook',
      'GET /appraise': 'CaptainController.appraiseTreasure', // <-- ADD THIS LINE
      '/': { view: 'pages/homepage' },
      // ... rest of the file
      
  • Task 5: See the Expert at Work

    1. Restart your server with sails lift.
    2. Make sure you have at least one treasure in your database (you can create one using the method from Module 4 if needed). For example, a treasure with a value of 1000.
    3. Open your browser and navigate to http://localhost:1337/appraise.
    4. You should see a JSON response detailing the original value and the new, lower taxed value (e.g., 850).

Mission Debrief (Review & Outcomes)

Mission complete, Navigator. Your command structure is now far more robust. You’ve successfully delegated a complex task to a specialist, keeping your controller clean and your core logic organized and reusable.

By creating the AppraisalService, you’ve built an asset that can be called from anywhere. If the Crown’s Tax rate changes from 15% to 20%, you only have to change it in one place: the service file. Every part of your application that uses this service will be instantly updated.

This is the path to building large, professional applications. Your ability to organize code is just as important as your ability to write it.


Rewards

  • +200 Doubloons
  • Achievement Unlocked: You’ve successfully abstracted business logic into a reusable service. Your crew is growing more capable!
    • Badge Earned: Specialist Crew 🔧