Skip to content

Commit

Permalink
Added description of the Web API definition
Browse files Browse the repository at this point in the history
  • Loading branch information
oskardudycz committed Feb 24, 2024
1 parent c7d87ec commit bca1ace
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 109 deletions.
29 changes: 26 additions & 3 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ Such handlers should be defined per stream type (e.g., one for Shopping Cart, th

You could put such code, e.g. in your WebApi endpoint. Let's go to the next step and use that in practice in the real web application.

## Web Application
## Application Setup

Seems like we have our business rules modelled, business logic reflected in code, and even tested. You also know how to write application code for handling commands. Isn't that cool? That's nice, but we need to build real applications, which nowadays typically mean a Web Application. Let's try to do it as well.

Expand Down Expand Up @@ -255,9 +255,14 @@ To configure API, we need to provide router configuration. We can do it via the

<<< @./../packages/emmett-expressjs/src/index.ts#web-api-setup

We recommend providing different web app configurations for different logical groups. It's also worth injecting all needed dependencies from the top, as that will make integration testing easier.
We recommend providing different web app configurations for different endpoints' logical groupings. It's also worth injecting all needed dependencies from the top, as that will make integration testing easier.

That's what we did in our case. We've set up our Shopping Carts API and injected the event store. That clearly explains what dependencies this API needs, and by reading the file, you can understand what your application technology needs. That should cut the onboarding time for new people grasping our system setup.
That's what we did in our case. We've set up our Shopping Carts API and injected external dependencies:

- event store to store and retrieve events,
- The `getUnitPrice` method represents a call to an external service to get the price of a product added to the shopping cart.

That clearly explains what dependencies this API needs, and by reading the file, you can understand what your application technology needs. That should cut the onboarding time for new people grasping our system setup.

<<< @/snippets/gettingStarted/webApi/apiSetup.ts#getting-started-api-setup

Expand All @@ -266,3 +271,21 @@ We're using the simplest option for this guide: an in-memory event store. For a
Sounds like we have all the building blocks to define our API; let's do it!

## WebAPI definition

Let's define our Shopping Cart WebApi. As mentioned before, we'll need two external dependencies: event store and query for product price:

<<< @/snippets/gettingStarted/webApi/shoppingCartApiSetup.ts#getting-started-api-setup

The API definition is a function taking external dependencies and returning the Web API setup. We're also setting up [Command Handler (as explained in the previous section)](#command-handling). Let's not keep it empty for too long and define our first endpoint!

We'll start with adding product items to the shopping cart and _vanilla_ Express.js syntax.

<<< @/snippets/gettingStarted/webApi/shoppingCartEndpointVanilla.ts#getting-started-vanilla-router

::: info Web Api Command Handling can be described by the following steps:

1. **Translate and request params to the command.** This is also a place to run necessary validation. Thanks to that, once we created our command, we can trust that it's validated and semantically correct. We don't need to repeat that in the business logic. That reduces the number of IFs and, eventually, the number of unit tests.
2. **Run command handler on top of business logic.** As you see, we keep things explicit; you can still run _Go to definition_ in your IDE and understand what's being run. So we're keeping things that should be explicit, explicit and hiding boilerplate that can be implicit.
3. **Return the proper HTTP response.**

:::
6 changes: 5 additions & 1 deletion docs/snippets/gettingStarted/webApi/apiSetup.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { shoppingCartApi } from './simpleApi';

const getUnitPrice = (_productId: string) => {
return Promise.resolve(100);
};

// #region getting-started-api-setup
import { getInMemoryEventStore } from '@event-driven-io/emmett';

const eventStore = getInMemoryEventStore();

const shoppingCarts = shoppingCartApi(eventStore);
const shoppingCarts = shoppingCartApi(eventStore, getUnitPrice);
// #endregion getting-started-api-setup
76 changes: 76 additions & 0 deletions docs/snippets/gettingStarted/webApi/shoppingCartApiSetup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
CommandHandler,
assertNotEmptyString,
assertPositiveNumber,
getInMemoryEventStore,
type EventStore,
} from '@event-driven-io/emmett';
import { NoContent, on } from '@event-driven-io/emmett-expressjs';
import { addProductItem } from '../businessLogic';
import type { AddProductItemToShoppingCart } from '../commands';
import { getShoppingCartId } from './simpleApi';

// #region getting-started-api-setup
import { type WebApiSetup } from '@event-driven-io/emmett-expressjs';
import { Router } from 'express';
import { evolve, getInitialState } from '../shoppingCart';

// Let's setup the command handler, we'll use it in endpoints
const handle = CommandHandler(evolve, getInitialState);

export const shoppingCartApi =
(
// external dependencies
eventStore: EventStore,
getUnitPrice: (productId: string) => Promise<number>,
): WebApiSetup =>
(router: Router): void => {
// We'll setup routes here
};

// #endregion getting-started-api-setup

const router: Router = Router();

const getUnitPrice = (_productId: string) => {
return Promise.resolve(100);
};

const eventStore = getInMemoryEventStore();

import { type Request } from 'express';

type AddProductItemRequest = Request<
Partial<{ clientId: string; shoppingCartId: string }>,
unknown,
Partial<{ productId: number; quantity: number }>
>;

router.post(
'/clients/:clientId/shopping-carts/current/product-items',
on(async (request: AddProductItemRequest) => {
const shoppingCartId = getShoppingCartId(
assertNotEmptyString(request.params.clientId),
);
const productId = assertNotEmptyString(request.body.productId);

const command: AddProductItemToShoppingCart = {
type: 'AddProductItemToShoppingCart',
data: {
shoppingCartId,
productItem: {
productId,
quantity: assertPositiveNumber(request.body.quantity),
unitPrice: await getUnitPrice(productId),
},
},
};

await handle(eventStore, shoppingCartId, (state) =>
addProductItem(command, state),
);

return NoContent();
}),
);
61 changes: 61 additions & 0 deletions docs/snippets/gettingStarted/webApi/shoppingCartEndpointVanilla.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
assertNotEmptyString,
assertPositiveNumber,
getInMemoryEventStore,
} from '@event-driven-io/emmett';
import { addProductItem } from '../businessLogic';
import type { AddProductItemToShoppingCart } from '../commands';
import { getShoppingCartId, handle } from './simpleApi';

import { Router } from 'express';

const router: Router = Router();

const getUnitPrice = (_productId: string) => {
return Promise.resolve(100);
};

const eventStore = getInMemoryEventStore();

// #region getting-started-vanilla-router
import type { Request, Response } from 'express';

type AddProductItemRequest = Request<
Partial<{ clientId: string; shoppingCartId: string }>,
unknown,
Partial<{ productId: number; quantity: number }>
>;

router.post(
'/clients/:clientId/shopping-carts/current/product-items',
async (request: AddProductItemRequest, response: Response) => {
// 1. Translate request params to the command
const shoppingCartId = getShoppingCartId(
assertNotEmptyString(request.params.clientId),
);
const productId = assertNotEmptyString(request.body.productId);

const command: AddProductItemToShoppingCart = {
type: 'AddProductItemToShoppingCart',
data: {
shoppingCartId,
productItem: {
productId,
quantity: assertPositiveNumber(request.body.quantity),
unitPrice: await getUnitPrice(productId),
},
},
};

// 2. Handle command
await handle(eventStore, shoppingCartId, (state) =>
addProductItem(command, state),
);

// 3. Send response status
response.sendStatus(204);
},
);

// #endregion getting-started-vanilla-router
8 changes: 6 additions & 2 deletions docs/snippets/gettingStarted/webApi/simpleApi.int.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@ import assert from 'node:assert/strict';
import { beforeEach, describe, it } from 'node:test';
import request from 'supertest';
import { v4 as uuid } from 'uuid';
import { getShoppingCartId, shoppingCartApi } from './simpleApi';
import type { ShoppingCartEvent } from '../events';
import { getShoppingCartId, shoppingCartApi } from './simpleApi';

const getUnitPrice = (_productId: string) => {
return Promise.resolve(100);
};

describe('Simple Api from getting started', () => {
let app: Application;
let eventStore: EventStore;

beforeEach(() => {
eventStore = getInMemoryEventStore();
app = getApplication({ apis: [shoppingCartApi(eventStore)] });
app = getApplication({ apis: [shoppingCartApi(eventStore, getUnitPrice)] });
});

it('Should handle requests correctly', async () => {
Expand Down
Loading

0 comments on commit bca1ace

Please sign in to comment.