Skip to main content

API

API endpoints form the access points into our application. They allow frontend applications to send requests to our backend, which are then converted to commands and finally return a response back to the client. This section here is discussing POST requests.

Implementation

We split the API into two parts:

  1. Infrastructure Agnostic API File: This handles the core API logic.
  2. Handler File: This translates AWS-specific events into technology-agnostic requests.

API Class

The API file is located at: src/Task/src/useCases/write/AddTask/infrastructure/AddTaskApi.ts.

import { AddTaskCommand } from "../application/AddTaskCommand";
import { AddTaskCommandHandler } from "../application/AddTaskCommandHandler";
import { ValidationError, OverwriteProtectionBody, NotFoundError, ConflictError, PreconditionFailedError } from "@codebricks/codebricks-framework";
import { TaskAggregate } from "shared/domain/aggregate/TaskAggregate";
import { TitleValueObject } from "shared/domain/valueObjects/TitleValueObject";
import { DescriptionValueObject } from "shared/domain/valueObjects/DescriptionValueObject";
import { AssigneeIdValueObject } from "shared/domain/valueObjects/AssigneeIdValueObject";

export interface AddTaskApiRequest {
title: string;
description: string;
assigneeId: string;
}

export interface AddTaskApiResponseBody {
taskId: string;
}

export interface AddTaskApiResponse {
statusCode: number;
body: string;
headers: any;
}

export class AddTaskApi {
constructor(readonly commandHandler: AddTaskCommandHandler = new AddTaskCommandHandler()) {
}

@OverwriteProtectionBody(false)
async handle(request: AddTaskApiRequest): Promise<AddTaskApiResponse> {
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true
};
try {
const command: AddTaskCommand = new AddTaskCommand({
title: new TitleValueObject(request.title),
description: new DescriptionValueObject(request.description),
assigneeId: new AssigneeIdValueObject(request.assigneeId),
});
const aggregate: TaskAggregate = await this.commandHandler.handleAddTask(command);
const responseBody: AddTaskApiResponseBody = {
taskId: aggregate.id,
};
return {
statusCode: 200,
body: JSON.stringify({ data: responseBody }),
headers: headers
};
} catch (error) {
console.log(error);
if (error instanceof ValidationError) {
return {
statusCode: 400,
body: JSON.stringify({ error: error.message }),
headers: headers
};
} else if (error instanceof SyntaxError && error.message.match(/Unexpected.token.*JSON.*/i)) {
return {
statusCode: 400,
body: '{ "error": "bad request: invalid json"}',
headers: headers
};
} else if (error instanceof NotFoundError) {
return {
statusCode: 404,
body: JSON.stringify({ error: error.message }),
headers: headers
};
} else if (error instanceof ConflictError) {
return {
statusCode: 409,
body: JSON.stringify({ error: error.message }),
headers: headers
};
} else if (error instanceof PreconditionFailedError) {
return {
statusCode: 412,
body: JSON.stringify({ error: error.message }),
headers: headers
};
}

return {
statusCode: 500,
body: '{ "error": "Internal Server Error"}',
headers: headers
};
}
}
}

In addition to the API class, it contains interfaces for the API Request and the API Response.

Key Functions of the API Class

  • Convert API Request to Command: Transforms incoming API requests into commands.
  • Pass Command to Command Handler: Sends the command to the appropriate command handler.
  • Build API Response: Constructs the API response from the aggregate returned by the command handler.
  • Render Errors: Converts errors thrown by the command handler into an API response.
Error Rendering

Additional errors should be handled here to render custom codes.

API Handler

The API handler file is located at: src/Task/src/useCases/write/AddTask/infrastructure/AddTaskApiHandler.ts.

It serves as an entry point into the Lambda function and translates AWS API Gateway events into vendor-agnostic requests.

import { APIGatewayProxyEvent, APIGatewayProxyResult } from "@codebricks/codebricks-framework";
import { AddTaskApi } from "./AddTaskApi";
import { AddTaskApiRequest } from "./AddTaskApi";
import { initDataSource } from "shared/infrastructure/persistence/AppDataSource";

/** @overwrite-protection-body false */
export async function handler(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> {
try {
await initDataSource();
const addTaskApi: AddTaskApi = new AddTaskApi();
const request: AddTaskApiRequest = JSON.parse(event.body ? event.body : '{}') as AddTaskApiRequest;
return await addTaskApi.handle(request) as unknown as Promise<APIGatewayProxyResult>;
} catch (error: any) {
console.log(error);
if (error instanceof SyntaxError && error.message.match(/Unexpected.token.*JSON.*/i)) {
return Promise.resolve({
statusCode: 400,
body: '{ "error": "bad request: invalid json"}',
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true
}
}) as Promise<APIGatewayProxyResult>;
} else{
return Promise.resolve({
statusCode: 500,
body: '{ "error": "Internal Server Error"}',
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true
}
}) as Promise<APIGatewayProxyResult>;
}
}
}

Best Practices

  • Security: Implement authentication and authorization mechanisms.
  • Error Rendering: Consistently handle and log errors to facilitate debugging.