Skip to content

Commit

Permalink
Completed documentation for the WebApi definition
Browse files Browse the repository at this point in the history
  • Loading branch information
oskardudycz committed Feb 24, 2024
1 parent bca1ace commit 4394de0
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 0 deletions.
35 changes: 35 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,38 @@ We'll start with adding product items to the shopping cart and _vanilla_ Express
3. **Return the proper HTTP response.**

:::

As you see, it's just a regular Express.js syntax. Still, in the longer term, it's better to have a way to make it a more scalable approach and unify intended usage patterns. That's what we're here for, right?

In Emmett, you can define it also like that:

<<< @/snippets/gettingStarted/webApi/shoppingCartEndpointWithOn.ts#getting-started-on-router

`on` is a simple wrapper:

<<< @./../packages/emmett-expressjs/src/handler.ts#httpresponse-on

But this simplicity is powerful as:

- it makes code more explicit on what we have as input and what is output. Emmett also defines the explicit signatures for the most common
- unifies request processing, which should enable better handling of telemetry, logging, OpenApi, etc.
- enables keeping endpoint handlers even in different files, so enables organisation,
- you could even unit test it without running the whole application.

If you still don't buy that, check a more advanced scenario showing a different flow, where shopping cart opening should happen explicitly:

<<< @./../packages/emmett-expressjs/src/e2e/decider/api.ts#created-example

**Yes, Emmett provides more built-in response helpers together with the explicit options.** Created will generate the location header. If you're returning the error status (e.g. `404 Not Found`), you can add problem details, information, etc.

**What's also sweet is that you can use Emmett's Express.js helpers even without an Event Sourcing code; Bon Appétit!**

Still, we're here for the Event Sourcing, so let's see the whole API:

<<< @/snippets/gettingStarted/webApi/simpleApi.ts#complete-api

Of course, we could make it even crisper and automagically do the request mapping, more conventional-based status resolution, decorators, and fire-command-and-forget, but we won't. Why?

**Emmett prefers composability over magical glue.** We believe that a healthy amount of copy-paste won't harm you. We target removability and segregation of the code and making things explicit that should be explicit.

**Still, Emmett won't tell you how to live!** If you want to add more, feel free to do it. We want to give you basic building blocks and recommendations so you can build on top of that!
1 change: 1 addition & 0 deletions docs/snippets/gettingStarted/webApi/apiSetup.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { shoppingCartApi } from './simpleApi';

Expand Down
62 changes: 62 additions & 0 deletions docs/snippets/gettingStarted/webApi/shoppingCartEndpointWithOn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
CommandHandler,
assertNotEmptyString,
assertPositiveNumber,
getInMemoryEventStore,
} from '@event-driven-io/emmett';
import { NoContent, on } from '@event-driven-io/emmett-expressjs';
import { Router, type Request } from 'express';
import { addProductItem } from '../businessLogic';
import type { AddProductItemToShoppingCart } from '../commands';
import { evolve, getInitialState } from '../shoppingCart';
import { getShoppingCartId } from './simpleApi';

const handle = CommandHandler(evolve, getInitialState);
const router: Router = Router();

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

const eventStore = getInMemoryEventStore();

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

// #region getting-started-on-router
router.post(
'/clients/:clientId/shopping-carts/current/product-items',
on(async (request: AddProductItemRequest) => {
// 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. Return response status
return NoContent();
}),
);

// #endregion getting-started-on-router
3 changes: 3 additions & 0 deletions docs/snippets/gettingStarted/webApi/simpleApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const shoppingCartApi =
getUnitPrice: (_productId: string) => Promise<number>,
): WebApiSetup =>
(router: Router) => {
// #region complete-api
// Add Product Item
router.post(
'/clients/:clientId/shopping-carts/current/product-items',
on(async (request: AddProductItemRequest) => {
Expand Down Expand Up @@ -132,6 +134,7 @@ export const shoppingCartApi =
return NoContent();
}),
);
// #endregion complete-api
};

// Add Product Item
Expand Down
2 changes: 2 additions & 0 deletions packages/emmett-expressjs/src/e2e/decider/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const dummyPriceProvider = (_productId: string) => {

export const shoppingCartApi = (eventStore: EventStore) => (router: Router) => {
// Open Shopping cart
// #region created-example
router.post(
'/clients/:clientId/shopping-carts/',
on(async (request: Request) => {
Expand All @@ -47,6 +48,7 @@ export const shoppingCartApi = (eventStore: EventStore) => (router: Router) => {
});
}),
);
// #endregion created-example

router.post(
'/clients/:clientId/shopping-carts/:shoppingCartId/product-items',
Expand Down
2 changes: 2 additions & 0 deletions packages/emmett-expressjs/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type NoContentHttpResponseOptions,
} from '.';

// #region httpresponse-on
export type HttpResponse = (response: Response) => void;

export type HttpHandler<RequestType extends Request> = (
Expand All @@ -28,6 +29,7 @@ export const on =

return setResponse(response);
};
// #endregion httpresponse-on

export const OK =
(options?: HttpResponseOptions): HttpResponse =>
Expand Down

0 comments on commit 4394de0

Please sign in to comment.