Skip to content

Backend API

qreoct edited this page Apr 14, 2021 · 1 revision

This document describes the backend API architecture, as well as certain guidelines to keep in mind when creating new endpoints.

Table of contents

  1. Overview of Backend architecture
  2. Extending new endpoints
  3. Writing documentation

1. Overview

The Backend API handles API calls from the frontend.

Model View Controller (MVC)

The backend is structured using the MVC design architecture.

Views are responsible for retrieving and showing the data (such as converting results from our PostgreSQL tables to data structures that the backend can manipulate). Can be found in lib/cadet_web/admin_views and lib/cadet_web/views.

Example: the IncentivesView in lib/cadet_web/views/incentives_view.ex takes in data (formatted in JSON) and renders them to a format that Elixir can manipulate.

Models are data structures which can hold information that other parts of the application uses. Can be found in lib/cadet

Example: the Sublanguage model in lib/cadet/settings/sublanguage.ex specifies the fields that the Source Academy Playground sublanguage can be modelled as.

Controllers accept and manage requests from users, modify a model (if required), and convert it into a view (if required). Can be found in lib/cadet_web/admin_controllers and lib/cadet_web/controllers. The API endpoints will typically interact with Controllers, and their functions are documented automatically in Swagger.

Example: the Auth controller in lib/cadet_web/controllers/auth_controller.ex manages the various functions to deal with authorization: creating a connection (logging in), refreshing the token, as well as logging out. It interacts with the User model.

Structure

/lib

The main bulk of functionality is here. The Models can be found in /cadet, and Controllers and Views are found in /cadet_web.

/test

All test files go here.

Illustration

For purpose of illustration, we will give an example of how the Frontend makes an API call to log in a user, and how it's handled in the Backend.

  1. A user tries to log in to Source Academy. The frontend file, src/commons/sagas/RequestsSaga.ts, makes a HTTP request to POST /auth/login.
  2. The backend router, lib/cadet_web/router.ex, manages this request. It calls the create method in AuthController.
  3. Within AuthController, lib/cadet_web/controllers/auth_controller.ex, the create method calls the sign_in function from the Accounts model (lib/cadet/accounts/accounts.ex).
  4. Assuming the sign_in function returns an 'ok' response, our AuthController will then make a call to the Auth view (lib/cadet_web/views/auth_view.ex) which renders the JSON web token.
  5. Finally, this web token is returned as a response to the HTTP request.

2. Extending the backend API

New routes are added to the lib/cadet_web/router.ex file.

Admin or public route?

We use the /admin route when we need authorization in the form of staff/admin. Specifically. those authorized are:

  • Teaching staff (Professors & Avengers)
  • Site administrators

Those unauthorized are:

  • Students
  • Public access (e.g. users of the Github playground)

For /admin endpoints, authorized users will get data as specified. Unauthorized users will receive HTTP 403 Forbidden responses, unauthenticated users will receive HTTP 401 Unauthorized.

For public-facing endpoints, if users are authenticated, only user-specific data can be returned (i.e. no access to other users’ information).

Creating new Controllers

Controllers must be prepended with 'Admin' if it conforms to admin-related tasks. Example: AdminAssetsController. Similarly, filenames are separated by underscores, such as admin_assets_controller.ex

If the Controller is a noun, use its plural term (e.g. Notifications, Achievements etc.)

Testing

When new endpoints are created or new functionality is implemented, please add the appropriate tests for the component.

Tests should cover:

  • Authorization checks and that proper responses are sent for unauthorized users
  • Expected functionality for dummy requests
  • Expected functionality for invalid requests (e.g. missing parameters, invalid parameter type etc.)
  • Create migrations which that have some initial data that is used for local development purposes

3. Writing documentation

Documenting Controllers

Controllers are documented using Swagger.

Within each Controller, specify:

  1. swagger_path, which describes the endpoint. An example from notifications_controller.ex:
  swagger_path :index do
    get("/notifications")

    summary("Get the unread notifications belonging to a user")

    security([%{JWT: []}])

    produces("application/json")

    response(200, "OK", Schema.ref(:Notification))
    response(401, "Unauthorised")
  end

The above code documents the :index method within the Notifications controller, specifying that it is a GET endpoint. We prefer the summary to be a short description of the endpoint, and not a full sentence, so we leave out the period. We specify response type and possible HTTP responses, along with corresponding schema (see below) of the response.

For path endpoint with variables, use /endpoint/{variableInCamelCase} instead of /endpoint/:variableInCamelCase.

For endpoint with multiple words, split using underscores. Examples: PUT /user/game_states, GET /devices/{secret}/mqtt_endpoint

  1. Additional Schemas which represent the response. An example from notifications_controller.ex:
  def swagger_definitions do
    %{
      Notification:
        swagger_schema do
          title("Notification")
          description("Information about a single notification")

          properties do
            id(:integer, "the notification id", required: true)
            type(:string, "the type of the notification", required: true)
            read(:boolean, "the read status of the notification", required: true)

            assessmentId(:integer, "the submission id the notification references", required: true)

            questionId(:integer, "the question id the notification references")

            assessment(Schema.ref(:AssessmentInfo))
          end
        end
    }
  end

Properties of each field can have type specification as well as a short description. The schema can also link to other schemas defined elsewhere, like the reference to :AssessmentInfo here

Documenting Tests

  • Use descriptive test descriptions. e.g. if testing endpoints, specify the HTTP request, route, and authentication status, such as describe "DELETE /:assessment_id, unauthenticated"
  • Use is_nil rather than == nil
  • Use assert when checking for positive conditions (i.e. something == true) and refute when checking for negative conditions (i.e. something == false)