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:
- Infrastructure Agnostic API File: This handles the core API logic.
- 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.
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.