ApiController

ApiController is a specialized controller used to create a Restful API using convention over configuration.

Implementation

To create a Restful API, create a class inherited from ApiController

import { ApiController } from "kamboja"
 
export class ItemsController extends ApiController {
    get(id:string) {
        return "Hello world!"
    }
}

Thats what you need, above class will generate

GET /items/:id

notice Controller name will be removed automatically. a namespace can be added to add additional path

import { ApiController } from "kamboja"

namespace Api.v1 {
    export class ItemsController extends ApiController {
        get(id:string) {
            return "Hello world!"
        }
    }
}

Above code will generate

GET /api/v1/items/:id

Actions

Above example we use get(id) action, get is a special method that will be translated to specific url follow the Restful API best practice.

Below is list of special action/method and its generation

Action Method Url
get(id, <optional>) GET /items/:id?<optional>
list(<optional>) GET /items?<optional>
add(body, <optional>) POST /items?<optional>
replace(id, body, <optional>) PUT /items/:id?<optional>
modify(id, body, <optional>) PATCH /items/:id?<optional>
delete(id, <optional>) DELETE /items/:id?<optional>

Parameter Order & Rules

KambojaJS doesn’t limit you to named the parameter, but on parameter binding process there are rules defined

Method Rule
get(id) id should be the first parameter
list() has no rule, you can add any parameter
add(data) first parameter will be populated with Request Body
replace(id, data) id should be first parameter, data will be populated with Request Body
modify(id, data) id should be first parameter, data will be populated with Request Body
delete(id) id should be the first parameter

You are free to give additional parameter and the parameter name.

get(email, otherId, andOther)

above code generated into:

GET /items/:email?otherId=value&andOther=value

or

replace(email, data)

Above action will generate

PUT /items/:email
    <Request Body goes to data parameter>

Read more about parameter in this link

Default Required Parameters

All special parameters id and data is automatically attached with @val.required() validation decorator. So by default KambojaJS will detect the presence of id and data automatically.

Method Validation
get(id) id is required by default
list() Has no required parameter by default
add(data) data is required by default
replace(id, data) id and data is required by default
modify(id, data) id and data is required by default
delete(id) id is required by default

Nested Resources

KambojaJS provided a convenient way to create Restful API with nested resources for example you have Restful API below:

POST /companies
GET  /companies/<companyid>
POST /companies/<companyid>/employees/
GET  /companies/<companyid>/employees/<employeeid>

To handle above url you need to create two separate controller, the first one to handle the /companies the seconds to handle /companies/<companyid>/employees/ like below:

import { ApiController, http } from "kamboja"

export class CompaniesController extends ApiController {
    add(body) {
        
    }

    get(companyId){

    }
}

@route.root("/companies/:companyId/employees")
export class CompaniesEmployeesController extends ApiController {
    add(body, companyId) {
        
    }

    get(companyId, id){
        
    }
}

There are two things important in above code.

  1. @route.root() will assigned a new name to CompaniesEmployeesController thus, instead of having /companiesemployees url, it has a new name /companies/:companyId/employees notice the :companyId parameter.

  2. companyId parameter at the end parameter of the add and get action, automatically populated from the /companies/:companyId/employees url.

so, when accessed using /companies/123/employees/456 using GET http method, the get method of CompaniesEmployeesController will be called and companyId parameter populated with 123 and the id parameter populated with value 456

Return Value

You can return any kind of value from ApiController action including Promise example:

Return object

import { ApiController } from "kamboja"

export class ItemsController extends ApiController {
    get(itemId){
        return { name: "iPhone 6s Plus" }
    }
}

Return promise using mongoose model

import { ApiController } from "kamboja"
import { ItemModel } from "./mongoose/item-model"

export class ItemsController extends ApiController {
    get(itemId){
        return ItemModel.findById(itemId).exec()
    }
}

using async/await

import { ApiController } from "kamboja"
import { ItemModel, UserModel } from "./mongoose/item-model"

export class ItemsController extends ApiController {
    async get(itemId){
        let item = await ItemModel.findById(itemId).exec()
        let user = await UserModel.findById(item.createdBy).exec()
        return {
            item: item, createdBy: user
        }
    }
}

return void

import { ApiController } from "kamboja"
import { ItemModel, UserModel } from "./mongoose/item-model"

export class ItemsController extends ApiController {
    async add(data){
        let itemODM = new ItemModel(data)
        await itemODM.save()
    }
}

All above code will return valid HTTP request with status code 200 (OK)

Return HTTP Status

Another best practice on Restful API is using HTTP Status in certain error. for example:

  1. 400 Bad request (issue with data, validation etc)
  2. 401 Unauthorized (user not login)
  3. 403 Forbidden (user login, but don’t have privilege)
  4. 404 Resource not found

You can throw HttpStatusError inside the action to send specific Http Status. This also have beneficial when unit testing the action.

Example below:

import { ApiController, HttpStatusError } from "kamboja"
import { ItemModel, UserModel } from "./mongoose/item-model"

export class ItemsController extends ApiController {
    async modify(id, data){
        let item = await ItemModel.count({_id: id}).exec()
        if(item == 0) throw new HttpStatusError(404, "Specified item does not exists")
        await ItemModel.update({_id:id}, data)
    }
}

The error flow kept natural by throwing HttpStatusError, and it have beneficial on unit testing.

Return Action Result

In some case if you want to return a status code but it is not currently an error, for example 201 (Created), 202(Accepted) etc, you can return any ActionResult derived class.

import { ApiController, Core } from "kamboja"
import { ItemModel, UserModel } from "./mongoose/item-model"

export class ItemsController extends ApiController {
    async add(data){
        let actionResult = new Core.ActionResult("Successfully created!")
        actionResult.status = 201
        return actionResult
    }
}

See more info about action result here