Saltearse al contenido
Corsace Documentation
DiscordGitHubTwitchTwitterYouTube

Server + CronRunner - Creating API Endpoints

This explains how to write functionality for creating new API endpoints.
You typically want to create endpoints in order to obtain any certain kind of data that is not provided by any other current endpoints that exist.
Most endpoints typically are for providing a means of communication between the client apps/services that exist in the Corsace ecosystem to the database server, but some may be created and used for other purposes such as for obtaining static data and communicating with other server-run programs.

Prerequisite Reading

Directory Structure

The primary code related to creating api endpoints and the API server itself is in Server/api.
While the file entry-point is at Server/index.ts, that file is not primarily relevant to creating a new endpoint.
The Server/api/data folder can be used to store any form of data that may be from external sources, but are also static.
The entire Interfaces folder also exists to contain the general typing and interfaces needed to send data between the server to clients.

Server Endpoint Anatomy

Server Endpoint Location

There will typically be 3 situations where you would be in when creating a new endpoint:

  • Adding a new endpoint into a currently existing file
  • Creating a new file for a new set of endpoints
  • Creating a new folder for a new set of files for a new set of endpoints

Preexisting Route

If the endpoint will be a certain instance that will not contextually branch into further endpoints, or would branch but is a dynamic route (such as a params query like having :tournamentID in the route) you can add it to an existing file.

Make sure that if you are adding an endpoint, that they would be placed above any dynamic routes with the same HTTP request method. For example:

// /open is not a dynamic route
tournamentRouter.$get<{ tournament: Tournament }>("/open/:year", async (ctx) => { ... });

// /validateKey is not a dynamic route
tournamentRouter.$get<{ tournamentID?: number }>("/validateKey", async (ctx) => { ... });

// ... Your endpoint would go here if it does not contain a dynamic route

// /:tournamentID/teams is a dynamic route. This is placed under the previous two routes in the file.
tournamentRouter.$get<{ teams: TeamList[] }>("/:tournamentID/teams", validateID, async (ctx) => { ... });

// ... Your endpoint would go here if it contains a dynamic route

Creating a New File/Folder

In the case where you need a route for your endpoint that will also possibly branch off to other endpoints (that you may also want to make at the same time, or in the future), you would create either a new file, or a new folder with a new file. The choice you make depends on your specific case and context.

Examples

If you are creating endpoints for teams that will require multiple endpoints for creating, updating, deleting teams, e.t.c, and the base api url would be /api/teams, you would create a new file called teams.ts in the Server/api/routes folder.

If you are creating endpoints for team invites as well that would require multiple endpoints for creating, updating, deleting invites, e.t.c, and the base api url would be /api/teams/invites, you would create a new folder called teams in the Server/api/routes folder, and then create a new file called invites.ts in the Server/api/routes/teams folder. The previous teams.ts file would be then be moved into a file called index.ts in the Server/api/routes/teams folder.

Middlewares

Server Endpoint Functionality

Typically, you will want to create a router using the CorsaceRouter class detailed in /Server/corsaceRouter.ts. This is a custom extension of the Router class in the koa-router package that allows for easier and more organized routing, and added functionality.

A file’s general structure would look like this:

// Server/api/routes/[ROUTERNAME].ts
import { CorsaceRouter } from "../../corsaceRouter";
import { [INTERFACE] } from "../../../Interfaces/[INTERFACE FILE LOCATION].ts";

const [ROUTERNAME]Router  = new CorsaceRouter();

// Endpoint for getting [RETURN TYPE]
// Please ensure you use $get instead of get, or $post instead of post, e.t.c
// To ensure that the endpoint is properly typed.
[ROUTERNAME]Router.$get<[RETURN TYPE]>("/[ENDPOINT]", (ctx) => {
    // Your endpoint logic here
    
    // Typical case where the endpoint is meant to fail/error
    ctx.body = {
        success: false,
        error: [ERROR MESSAGE]
    };
    return;

    // Typical case where the endpoint is meant to succeed
    // Example case for if [RETURN TYPE] is { data: [INTERFACE] }
    const obtainedData: [INTERFACE] = {...};
    ctx.body = {
        success: true,
        data: obtainedData,
    }
    return;
});

export default [ROUTERNAME]Router;

To go through each variable:

  • [INTERFACE] would be the interface that you would use to define the return type of the endpoint (if needed). This would be located in the Interfaces folder, and the [INTERFACE FILE LOCATION].ts would be the file path the interface is in.
  • [ROUTERNAME] would be the name of the router you are creating. This would be the name of the file/folder that the router is in. For example teamRouter for a file focusing on routes regarding team actions.
  • [RETURN TYPE] would be the return type of the endpoint. This would be the interface you would use to define the return type of the endpoint (if needed). This would be located in the Interfaces folder, and the [INTERFACE FILE LOCATION].ts would be the file path the interface is in.
    • An example of a [RETURN TYPE] value could be { [PROPERTYNAME]: [INTERFACE] } where [PROPERTYNAME] is the name of the property that would be returned, and [INTERFACE] is the interface that would be used to define the return type of the property. Such as { team: Team } where team is the property name, and Team is the interface that would be used to define the return type of the property.
    • Not applying a return type would mean that the endpoint would not return any data, and would only be used for server-side actions. Your IDE should be able to infer and enforce the return type if you do not specify it, and building the project should fail if the return type is not properly inferred.
  • [ENDPOINT] would be the endpoint that the router would be listening to. This would be the path that would be appended to the base api url. For example, if the base api url for the router is /team, and the endpoint is for getting a team, the [ENDPOINT] could be /. If it’s for something more specific such as for getting all teams, then [ENDPOINT] could be /all.

Exceptions

There are special cases where endpoints may not follow the general structure. Such cases could be redirects, or for more specific server-side use cases such as for the passport login system, or github webhook responses. In such cases, the general structure may not apply, and you may deviate as necessary to fit the specific use case.

In cases where the endpoints are meant to be used by Corsace services only, you should add the following before creating any endpoints so that the endpoints are protected by internal basic authentication:

...
import koaBasicAuth from "koa-basic-auth";
...
[ROUTERNAME]Router.$use(koaBasicAuth({
    name: config.interOpAuth.username,
    pass: config.interOpAuth.password,
}));

[ROUTERNAME]Router.$get<[RETURN TYPE]>("/[ENDPOINT]", (ctx) => { ... });
...

Installing Server Endpoint

In Server/index.ts, you would import the router you have created and append it to the server’s middleware like so:

...
import [ROUTERNAME]Router from "./api/routes/[ROUTERNAME]";
...
koa.use(Mount("/api/[ROUTERNAME]", [ROUTERNAME]Router.routes()));

// Place before the line in the file shown below
ormConfig.initialize()

Make sure that the line containing ormConfig.initialize() is placed after all the routers have been mounted, as the initialization of the ORM is dependent on the routers being mounted first.

Afterwards, the endpoint(s) created should be accessible through the base api url appended with the endpoint path whenever you run the api service. For example, if the base api url is http://localhost:3000/api, and the endpoint path is /team, the endpoint should now be accessible at http://localhost:3000/api/team.