API

The API is the access point into use cases of the type Command API and Query API:

  • The API of a Command API use case converts a POST request into a command and returns a response.
  • The API of a Query API use case converts a GET request into a query and returns a response.

Plan Command API

Request

Name

The Name is used to name the request, response interface in the code. It also defines the URL of the endpoint.

For more details, see Naming.

Actor

The Actor protects the Command API endpoint with user authentication.

Schema (request)

The Request Schema defines the properties of the Command API endpoint's request body.

For more details, see Schema.

Response

Schema (response)

The Name is used to name the request, response interface in the code. It also defines the URL of the endpoint.

For more details, see Naming.

Value Sources

The Response Schema Property Value Sources define how each property is initialized.

For more details, see Data Flow.

Addtionally Command API response can use the following Metadata Value Sources:

  • Aggregate Id: uuid - Id of the aggregate.
  • Aggregate Version: int - Version of the aggregate.
  • Aggregate Changed At: Datetime - Timestamp of the last Event applied to the aggregate.

Plan Query API

Request

Name

The Name is used to name the request, response interface in the code. It also defines the URL of the endpoint.

For more details, see Naming.

Actor

The Actor protects the Query API endpoint with user authentication.

Schema

The Request Schema defines the query parameters of the Query API endpoint's request.

For more details, see Schema.

Response

The Query API has no response sub-element.

Schema

The Query API's response body is defined by the schema and the query result type of the queried readmodel.

Implement Command API

The Command API is split into two parts:

  1. API File: Handles the core API logic.
  2. API Handler File: Is the entry point for the Lambda and translates AWS-specific events into vendor-agnostic requests.

API

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

It contains the API class and interfaces for the API Request and the API Response.

1import { AddTaskCommand } from "../application/AddTaskCommand";
2import { AddTaskCommandHandler } from "../application/AddTaskCommandHandler";
3import { ValidationError, OverwriteProtectionBody, NotFoundError, ConflictError, PreconditionFailedError } from "@codebricks/typebricks";
4import { TaskAggregate } from "shared/domain/aggregate/TaskAggregate";
5import { TaskTitleValueObject } from "shared/domain/valueObjects/TaskTitleValueObject";
6import { TaskDescriptionValueObject } from "shared/domain/valueObjects/TaskDescriptionValueObject";
7import { AssigneeIdValueObject } from "shared/domain/valueObjects/AssigneeIdValueObject";
8
9export interface AddTaskApiRequest {
10 title: string;
11 description: string;
12 assigneeId: string;
13}
14
15export interface AddTaskApiResponseBody {
16 taskId: string;
17}
18
19export interface AddTaskApiResponse {
20 statusCode: number;
21 body: string;
22 headers: any;
23}
24
25export class AddTaskApi {
26 constructor(readonly commandHandler: AddTaskCommandHandler = new AddTaskCommandHandler()) {
27 }
28
29 @OverwriteProtectionBody(false)
30 async handle(request: AddTaskApiRequest): Promise<AddTaskApiResponse> {
31 const headers = {
32 'Access-Control-Allow-Origin': '*',
33 'Access-Control-Allow-Credentials': true
34 };
35 try {
36 const command: AddTaskCommand = new AddTaskCommand({
37 title: new TaskTitleValueObject(request.title),
38 description: new TaskDescriptionValueObject(request.description),
39 assigneeId: new AssigneeIdValueObject(request.assigneeId),
40 });
41 const aggregate: TaskAggregate = await this.commandHandler.handleAddTask(command);
42 const responseBody: AddTaskApiResponseBody = {
43 taskId: aggregate.id,
44 };
45 return {
46 statusCode: 200,
47 body: JSON.stringify({ data: responseBody }),
48 headers: headers
49 };
50 } catch (error) {
51 console.log(error);
52 if (error instanceof ValidationError) {
53 return {
54 statusCode: 400,
55 body: JSON.stringify({ error: error.message }),
56 headers: headers
57 };
58 } else if (error instanceof SyntaxError && error.message.match(/Unexpected.token.*JSON.*/i)) {
59 return {
60 statusCode: 400,
61 body: '{ "error": "bad request: invalid json"}',
62 headers: headers
63 };
64 } else if (error instanceof NotFoundError) {
65 return {
66 statusCode: 404,
67 body: JSON.stringify({ error: error.message }),
68 headers: headers
69 };
70 } else if (error instanceof ConflictError) {
71 return {
72 statusCode: 409,
73 body: JSON.stringify({ error: error.message }),
74 headers: headers
75 };
76 } else if (error instanceof PreconditionFailedError) {
77 return {
78 statusCode: 412,
79 body: JSON.stringify({ error: error.message }),
80 headers: headers
81 };
82 }
83
84 return {
85 statusCode: 500,
86 body: '{ "error": "Internal Server Error"}',
87 headers: headers
88 };
89 }
90 }
91}

Key Functions of the API Class

  • Command Creation: Transforms an incoming API request into a command.
  • Command Handler Call: Sends the command to the corresponding 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.

API Handler - AWS

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.

1import { APIGatewayProxyEvent, APIGatewayProxyResult, parseToDateTime } from "@codebricks/typebricks";
2import { AddTaskApi } from "./AddTaskApi";
3import { AddTaskApiRequest } from "./AddTaskApi";
4import { initDataSource, destroyDataSource } from "shared/infrastructure/persistence/AppDataSource";
5
6/** @overwrite-protection-body false */
7export async function handler(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> {
8 try {
9 await initDataSource();
10 const addTaskApi: AddTaskApi = new AddTaskApi();
11 const request: AddTaskApiRequest = JSON.parse(event.body ? event.body : '{}', parseToDateTime) as AddTaskApiRequest;
12 return await addTaskApi.handle(request) as unknown as Promise<APIGatewayProxyResult>;
13 } catch (error: any) {
14 console.log(error);
15 if (error instanceof SyntaxError && error.message.match(/Unexpected.token.*JSON.*/i)) {
16 return Promise.resolve({
17 statusCode: 400,
18 body: '{ "error": "bad request: invalid json"}',
19 headers: {
20 'Access-Control-Allow-Origin': '*',
21 'Access-Control-Allow-Credentials': true
22 }
23 }) as Promise<APIGatewayProxyResult>;
24 } else {
25 return Promise.resolve({
26 statusCode: 500,
27 body: '{ "error": "Internal Server Error"}',
28 headers: {
29 'Access-Control-Allow-Origin': '*',
30 'Access-Control-Allow-Credentials': true
31 }
32 }) as Promise<APIGatewayProxyResult>;
33 }
34 } finally {
35 await destroyDataSource();
36 }
37}

Key Functions of the API Handler Class:

  • Initialization: Initializing the data source and Command API
  • Request Deserialization: Mapping API Gateway events to API requests.

Best Practices

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

Implement Query API

The Query API is split into two parts:

  1. API File: Handles the core API logic.
  2. API Handler File: Is the entry point for the Lambda and translates AWS-specific events into vendor-agnostic requests.

API

The Query API implementation is found at: Task/src/useCases/read/AssigneeTaskOverview/infrastructure/AssigneeTaskOverviewApi.ts.

1import { AddTaskCommand } from "../application/AddTaskCommand";
2import { AddTaskCommandHandler } from "../application/AddTaskCommandHandler";
3import { ValidationError, OverwriteProtectionBody, NotFoundError, ConflictError, PreconditionFailedError } from "@codebricks/typebricks";
4import { TaskAggregate } from "shared/domain/aggregate/TaskAggregate";
5import { TaskTitleValueObject } from "shared/domain/valueObjects/TaskTitleValueObject";
6import { TaskDescriptionValueObject } from "shared/domain/valueObjects/TaskDescriptionValueObject";
7import { AssigneeIdValueObject } from "shared/domain/valueObjects/AssigneeIdValueObject";
8
9export interface AddTaskApiRequest {
10 title: string;
11 description: string;
12 assigneeId: string;
13}
14
15export interface AddTaskApiResponseBody {
16 taskId: string;
17}
18
19export interface AddTaskApiResponse {
20 statusCode: number;
21 body: string;
22 headers: any;
23}
24
25export class AddTaskApi {
26 constructor(readonly commandHandler: AddTaskCommandHandler = new AddTaskCommandHandler()) {
27 }
28
29 @OverwriteProtectionBody(false)
30 async handle(request: AddTaskApiRequest): Promise<AddTaskApiResponse> {
31 const headers = {
32 'Access-Control-Allow-Origin': '*',
33 'Access-Control-Allow-Credentials': true
34 };
35 try {
36 const command: AddTaskCommand = new AddTaskCommand({
37 title: new TaskTitleValueObject(request.title),
38 description: new TaskDescriptionValueObject(request.description),
39 assigneeId: new AssigneeIdValueObject(request.assigneeId),
40 });
41 const aggregate: TaskAggregate = await this.commandHandler.handleAddTask(command);
42 const responseBody: AddTaskApiResponseBody = {
43 taskId: aggregate.id,
44 };
45 return {
46 statusCode: 200,
47 body: JSON.stringify({ data: responseBody }),
48 headers: headers
49 };
50 } catch (error) {
51 console.log(error);
52 if (error instanceof ValidationError) {
53 return {
54 statusCode: 400,
55 body: JSON.stringify({ error: error.message }),
56 headers: headers
57 };
58 } else if (error instanceof SyntaxError && error.message.match(/Unexpected.token.*JSON.*/i)) {
59 return {
60 statusCode: 400,
61 body: '{ "error": "bad request: invalid json"}',
62 headers: headers
63 };
64 } else if (error instanceof NotFoundError) {
65 return {
66 statusCode: 404,
67 body: JSON.stringify({ error: error.message }),
68 headers: headers
69 };
70 } else if (error instanceof ConflictError) {
71 return {
72 statusCode: 409,
73 body: JSON.stringify({ error: error.message }),
74 headers: headers
75 };
76 } else if (error instanceof PreconditionFailedError) {
77 return {
78 statusCode: 412,
79 body: JSON.stringify({ error: error.message }),
80 headers: headers
81 };
82 }
83
84 return {
85 statusCode: 500,
86 body: '{ "error": "Internal Server Error"}',
87 headers: headers
88 };
89 }
90 }
91}

Key Functions of the API Class:

  • Query Creation: Transforms incoming API requests into a Query.
  • Query Handler Call: Sends the Query to the appropriate Query handler.
  • Build API Response: Constructs the API response readmodels returned by the query handler.
  • Render Errors: Converts errors thrown by the query handler into an API response.

API Handler - AWS

The API Handler is located at: Task/src/useCases/read/AssigneeTaskOverview/infrastructure/AssigneeTaskOverviewApiHandler.ts.

1import { APIGatewayProxyEvent, APIGatewayProxyResult, parseToDateTime } from "@codebricks/typebricks";
2import { AddTaskApi } from "./AddTaskApi";
3import { AddTaskApiRequest } from "./AddTaskApi";
4import { initDataSource, destroyDataSource } from "shared/infrastructure/persistence/AppDataSource";
5
6/** @overwrite-protection-body false */
7export async function handler(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> {
8 try {
9 await initDataSource();
10 const addTaskApi: AddTaskApi = new AddTaskApi();
11 const request: AddTaskApiRequest = JSON.parse(event.body ? event.body : '{}', parseToDateTime) as AddTaskApiRequest;
12 return await addTaskApi.handle(request) as unknown as Promise<APIGatewayProxyResult>;
13 } catch (error: any) {
14 console.log(error);
15 if (error instanceof SyntaxError && error.message.match(/Unexpected.token.*JSON.*/i)) {
16 return Promise.resolve({
17 statusCode: 400,
18 body: '{ "error": "bad request: invalid json"}',
19 headers: {
20 'Access-Control-Allow-Origin': '*',
21 'Access-Control-Allow-Credentials': true
22 }
23 }) as Promise<APIGatewayProxyResult>;
24 } else {
25 return Promise.resolve({
26 statusCode: 500,
27 body: '{ "error": "Internal Server Error"}',
28 headers: {
29 'Access-Control-Allow-Origin': '*',
30 'Access-Control-Allow-Credentials': true
31 }
32 }) as Promise<APIGatewayProxyResult>;
33 }
34 } finally {
35 await destroyDataSource();
36 }
37}

Key Functions of the API Handler Class:

  • Initialization: Initializing the data source and Query API
  • Request Deserialization: Mapping API Gateway events to API requests.

Best Practices

  • Error Management: Implement detailed error handling to ensure clear and actionable error responses.
  • Performance: Optimize query handling and response generation for efficient processing.

© 2024 Codebricks | All rights reserved.