Part 3: Customizing APIs in Controller

Introduction

In this series, I’m going to help you learn LoopBack 4 and how to easily build your own API and web project with it. We’ll do so by creating a new project I’m working on: an online web text-based adventure game. In this game, you can create your own account to build characters, fight monsters and find treasures. You will be able to control your character to take a variety of actions: attacking enemies, casting spells, and getting loot. This game should also allow multiple players to log in and play with their friends.

Previously on Building an Online Game With LoopBack 4

In the last episode, we used a third-party library to generate UUID and built relations between character, weapon, armor, and skill.

Here are the previous episodes:

In This Episode

We already have some simple APIs in our project. They are all default CRUD (Create, Read, Update, and Delete) APIs that auto-generated by LoopBack 4. In this episode, we will create our own APIs to achieve the following functions for character updating:

models

  • The ability for users to equip their character with weapon, armor, and skill. This function should also be able to allow users to change weapon, armor, and skill for their character. In any of these cases, we should update defence and attack accordingly.
  • The ability for users to unequip their character. We also need to update defence and attack.
  • The ability to level up a character when it gets enough experience. We should update currentExp, nextLevelExp, level, maxHealth, currentHealth, maxMana, currentMana, attack, and defence.
  • The ability to check a character’s weapon, armor, and skill information.

Create Controller

First, let’s create a controller for updating your character. Run lb4 controller in your project root.

wenbo:firstgame wenbo$ lb4 controller
? Controller class name: UpdateCharacter
? What kind of controller would you like to generate? REST Controller with CRUD functions
? What is the name of the model to use with this CRUD repository? Character
? What is the name of your CRUD repository? CharacterRepository
? What is the type of your ID? string
? What is the base HTTP path name of the CRUD operations? /updatecharacter
   create src/controllers/update-character.controller.ts
   update src/controllers/index.ts

Controller UpdateCharacter was created in src/controllers/

Open /src/controllers/update-character.controller.ts. Add the following imports because this controller is associated with Armor, Weapon, skill as well.

import {Armor, Weapon, Skill} from '../models';
import {WeaponRepository, ArmorRepository, SkillRepository } from '../repositories';

Then add the following lines into constructor:

constructor(
  @repository(CharacterRepository)
  public characterRepository : CharacterRepository,

  //add following lines
  @repository(WeaponRepository)
  public weaponRepository : WeaponRepository,
  @repository(ArmorRepository)
  public armorRepository : ArmorRepository,
  @repository(SkillRepository)
  public skillRepository : SkillRepository,
) {}

This will connect this controller with Armor, Weapon, and skill. You can now delete all those default APIs since we don’t need them anymore.

Equip Character

The first API we need is @patch '/updatecharacter/{id}/weapon'. In this game, a character can only have one weapon. With that in mind, this API’s job is to equip characters with a weapon and unequip the old weapon if there is one.

Here is code for this API:

@patch('/updatecharacter/{id}/weapon', {
  responses: {
    '200': {
      description: 'update weapon',
      content: {'application/json': {schema: Weapon}},
    },
  },
})
async updateWeapon(
  @param.path.string('id') id: string,
  @requestBody() weapon: Weapon,
): Promise<Weapon> {
  //equip new weapon
  let char: Character = await this.characterRepository.findById(id);
  char.attack! += weapon.attack;
  char.defence! += weapon.defence;

  //unequip old weapon
  let filter: Filter = {where:{"characterId":id}};
  if((await this.weaponRepository.find(filter))[0] != undefined){
    let oldWeapon: Weapon = await this.characterRepository.weapon(id).get();
    char.attack! -= oldWeapon.attack;
    char.defence! -= oldWeapon.defence;
    await this.characterRepository.weapon(id).delete();
  }
  await this.characterRepository.updateById(id, char);
  return await this.characterRepository.weapon(id).create(weapon);
}

Let’s go over it line by line.

This is the function signature. It means this API expects to get a character ID from URL and weapon entity from body.

async updateWeapon(
  @param.path.string('id') id: string,
  @requestBody() weapon: Weapon,
): Promise<Weapon> {

  ...

The following lines will find the character entity from our database. Then we will update this character’s attack and defence. The ! after attack and defence tells the compiler we guarantee those variables are not undefined. Otherwise, we will get a compile error. In the weapon model, attack and defence are both required, so these cannot be empty.

//equip new weapon
let char: Character = await this.characterRepository.findById(id);
char.attack! += weapon.attack;
char.defence! += weapon.defence;

This block will check if this character already has a weapon. If so, it will update the character’s attack and defence and remove the old weapon from database.

//unequip old weapon
let filter: Filter = {where:{"characterId":id}};
if((await this.weaponRepository.find(filter))[0] != undefined){
  let oldWeapon: Weapon = await this.characterRepository.weapon(id).get();
  char.attack! -= oldWeapon.attack;
  char.defence! -= oldWeapon.defence;
  await this.characterRepository.weapon(id).delete();
}

Those two lines will update the update character information in our database and put the new weapon into it.

await this.characterRepository.updateById(id, char);
return await this.characterRepository.weapon(id).create(weapon);

We need to handle armor exactly the same. But skill is a little bit different, because in this game skill will not influence attack and defence. We just need to update our new skill and delete the old skill.

@patch('/updatecharacter/{id}/skill', {
  responses: {
    '200': {
      description: 'update skill',
      content: {'application/json': {schema: Skill}},
    },
  },
})
async updateSkill(
  @param.path.string('id') id: string,
  @requestBody() skill: Skill,
): Promise<Skill> {
  await this.characterRepository.skill(id).delete();
  return await this.characterRepository.skill(id).create(skill);
}

When we delete a character, we also need to delete its weapon, armor, and skill. To do this, open /src/controllers/character.controller.ts and add the following lines in del '/characters/{id} API.

@del('/characters/{id}', {
  responses: {
    '204': {
      description: 'Character DELETE success',
    },
  },
})
async deleteById(
  @param.path.string('id') id: string
): Promise<void> {
  //delete weapon, armor, and skill
  await this.characterRepository.weapon(id).delete();
  await this.characterRepository.armor(id).delete();
  await this.characterRepository.skill(id).delete();
  ///
  await this.characterRepository.deleteById(id);
}

Unequip Character

Unequipping the character is very easy.

For weapon and armor, simply remove them from database and update attack and defence.

@del('/updatecharacter/{id}/weapon', {
  responses: {
    '204': {
      description: 'DELETE Weapon',
    },
  },
})
async deleteWeapon(
  @param.path.string('id') id: string
): Promise<void> {
  //unequip old weapon
  let filter: Filter = {where:{"characterId":id}};
  if((await this.weaponRepository.find(filter))[0] != undefined){
    let oldWeapon: Weapon = await this.characterRepository.weapon(id).get();
    let char: Character = await this.characterRepository.findById(id);
    char.attack! -= oldWeapon.attack!;
    char.defence! -= oldWeapon.defence!;
    await this.characterRepository.weapon(id).delete();
    await this.characterRepository.updateById(id, char);
  }
}

For skill, just remove it from database.

@del('/updatecharacter/{id}/skill', {
  responses: {
    '204': {
      description: 'DELETE Skill',
    },
  },
})
async deleteSkill(
  @param.path.string('id') id: string
): Promise<void> {
    await this.characterRepository.skill(id).delete();
}

Levelling Up a Character

When a character has enough experience, we reward it by levelling up. In /src/controllers/update-character.controller.ts:

@patch('/updatecharacter/{id}/levelup', {
  responses: {
    '200': {
      description: 'level up',
      content: {'application/json': {schema: Character}},
    },
  },
})
async levelUp(@param.path.string('id') id: string): Promise<Character> {
    let char: Character = await this.characterRepository.findById(id);
    let levels: number = 0;
    while(char.currentExp! >= char.nextLevelExp!){
      levels++;
      char.currentExp! -= char.nextLevelExp!;
      char.nextLevelExp! += 100;
    }
    char.level! += levels;
    char.maxHealth! += 10 * levels;
    char.currentHealth! = char.maxHealth!;
    char.maxMana! += 5 * levels;
    char.currentMana! = char.maxMana!;
    char.attack! += 3 * levels;
    char.defence! += levels;
    await this.characterRepository!.updateById(id, char);
    return char;
}

Let’s go over this line by line.

If a character just beat a very strong enemy and gained a lot of experience, it could level up more than once. So the first thing we need to do is figure out how many times we need to level up.

let levels: number = 0;
while(char.currentExp! >= char.nextLevelExp!){
  levels++;
  char.currentExp! -= char.nextLevelExp!;
  char.nextLevelExp! += 100;
}

Then we can update everything accordingly.

char.level! += levels;
char.maxHealth! += 10 * levels;
char.currentHealth! = char.maxHealth!;
char.maxMana! += 5 * levels;
char.currentMana! = char.maxMana!;
char.attack! += 3 * levels;
char.defence! += levels;

Lastly, we update this character in database.

await this.characterRepository!.updateById(id, char);

Check Character Information

The last function we need to achieve is the ability to check character information.

Here is the code for this API:

@get('/updatecharacter/{id}', {
  responses: {
    '200': {
      description: 'armor, weapon, and skill info',
      content: {},
    },
  },
})
async findById(
  @param.path.string('id') id: string,
): Promise<any[]> {
  let res: any[] = ['no weapon', 'no armor', 'no skill'];

  let filter: Filter = {where:{"characterId":id}};
  if((await this.weaponRepository.find(filter))[0] != undefined){
    res[0] = await this.characterRepository.weapon(id).get()
  }
  if((await this.armorRepository.find(filter))[0] != undefined){
    res[1] = await this.characterRepository.armor(id).get()
  }
  if((await this.skillRepository.find(filter))[0] != undefined){
    res[2] = await this.characterRepository.skill(id).get()
  }
  return res;
}

We first create an array contains three elements: ‘no weapon’, ‘no armor’, ‘no skill’.

Then we will check our database. For example, if this character has a weapon, we will replace no weapon with the weapon information. Lastly, we return the array as result.

That is all we want to achieve in this episode. If you can follow all those steps, you should be able to try those API at http://[::1]:3000

You can check here for the code of this episode.

Applying This to Your Own Project

In this episode, we covered the how to customize APIs. You can always implement your own amazing idea in your LoopBack 4 project.

What’s Next?

In next episode, we will add user authentication and role-based access control to this project.

In the meantime, you can learn more about LoopBack in past blogs.