[QUESTION]: Adding Groups endpoints and Build Routes in a modular way
Issue Description:
Currently, there's a challenge regarding the modularity and scalability of routes within the presentation layer of the application, particularly when employing DDD (Domain-Driven Design). The goal is to enhance the architecture to better separate concerns and facilitate maintainability and extensibility.
To address this issue, we aim to refactor the code to achieve a more modular approach, allowing for greater flexibility and abstraction in handling routes.
there's a way to build the routes in a way to be modular with a native technique with Effect or there is a bit limitation for now of effect-http to do that? this is the structure of the folders 📦src ┣ 📂data-access ┃ ┗ 📜user.repository.ts ┣ 📂domain ┃ ┣ 📂user ┃ ┃ ┗ 📜user.model.ts ┃ ┗ 📜models.ts ┣ 📂entry-points ┃ ┣ 📜routes.ts ┃ ┗ 📜server.ts ┣ 📂presentation ┃ ┗ 📂user ┃ ┃ ┗ 📜user.route.ts ┣ 📂test ┃ ┗ 📜.gitkeep ┗ 📜env.ts
in order to be scalable and preserve the modularity i tried this way per example using DDD, having three layers, first the presentation layer, the domain layer and the data-layer, the main issue is in the presentation layer i need to pull up the implementation of the route away from the main pipe to run the effect
src/entry-points/server.ts
import { findAllUsers } from "@api-gateway-app/presentation/user/user.route";
import { NodeRuntime } from "@effect/platform-node";
import { Effect, Layer, LogLevel, Logger, pipe } from "effect";
import { RouterBuilder } from "effect-http";
import { NodeServer } from "effect-http-node";
import { PrettyLogger } from "effect-log";
import { AppRouter } from "./routes";
export const debugLogger = pipe(
PrettyLogger.layer(),
Layer.merge(Logger.minimumLogLevel(LogLevel.All))
)
pipe(
RouterBuilder.make(AppRouter, { docsPath: '/api', parseOptions: { errors: "all" } }),
RouterBuilder.handle("findAllUsers", ({ query }) => findAllUsers(query)), // ==== i want to move this handle away from
// the pipe ( the best i can do for now is this passing the callback with the implementation but isn't the goal )
RouterBuilder.build,
Effect.provide(debugLogger),
NodeServer.listen({ port: 3001 }),
NodeRuntime.runMain
)
src/entry-points/routes.ts
// this implementation of definition of the AppRouter is Ok because from here we
// can define from each scope int presentation layer in this case is user but can be
// added others scopes / collections
import { userApi } from "@api-gateway-app/presentation/user/user.route";
import { Api } from "effect-http";
export const AppRouter = Api.make({
title: "API Gateway",
servers: [{ url: "http://localhost:3001" }],
}).pipe(Api.addGroup(userApi));
import { Criteria, getPaginatedResponse } from "@api-gateway-app/domain/models";
import { User } from "@api-gateway-app/domain/user/user.model";
import { pipe } from "effect";
import { Effect } from "effect";
import { Api, ApiGroup, Security } from "effect-http";
// this is perfect because in the presentation layer we define the interface of the endpoint
export const userApi = pipe(
ApiGroup.make("Users", {
description: "All about Users",
}),
ApiGroup.addEndpoint(
ApiGroup.get("findAllUsers", "/api/users").pipe(
Api.setRequestQuery(Criteria),
Api.setResponseBody(getPaginatedResponse(User)),
Api.setSecurity(
Security.bearer({ name: "mySecurity", description: "test" })
),
)
)
);
// this is my concern and the ugly part (its working though) that only can
// export the callback of the implementation but no the actual implementation definition,
// this should be place the RouterBuilder.handle()
export const findAllUsers = (query: Criteria) => Effect.succeed({
data: [{ name: "mike", id: JSON.stringify(query) }],
cursor: "x",
hasMore: false,
});
there's a way to achieve this ? i'm new on Effect and I am very interested in learn and contribute on examples with TDD, DDD and monorepos with Effect
Thank you in advance!
Best regards,
https://github.com/sukovanej/effect-http?tab=readme-ov-file#router-handlers
https://github.com/sukovanej/effect-http?tab=readme-ov-file#router-handlers
That was very helpful, thank you! I'm going to leave here the example of the approach I took in a modular way, and it's pretty nice entrypoint/server.ts
import { NodeRuntime } from "@effect/platform-node";
import { Effect, Layer, LogLevel, Logger, pipe } from "effect";
import { NodeServer } from "effect-http-node";
import { AppRouterHandlers } from "./routes";
pipe(
AppRouterHandlers,
Effect.provide(debugLogger),
NodeServer.listen({ port: 3001 }),
NodeRuntime.runMain
)
entrypoint/routes.ts
import {
findAllUsers,
findUserById,
userApi,
} from "@api-gateway-app/presentation/user/user.route";
import { Api, RouterBuilder } from "effect-http";
const AppRouter = Api.make({
title: "API Gateway",
servers: [{ url: "http://localhost:3001" }],
}).pipe(Api.addGroup(userApi));
export const AppRouterHandlers = RouterBuilder.make(AppRouter, {
docsPath: "/api",
parseOptions: { errors: "all" },
}).pipe(
// Routes
// User Routes
RouterBuilder.handle(findAllUsers),
RouterBuilder.handle(findUserById),
RouterBuilder.build
);
presentation/user/user.route.ts
import { Criteria, getPaginatedResponse } from "@api-gateway-app/domain/models";
import { User } from "@api-gateway-app/domain/user/user.model";
import * as S from "@effect/schema/Schema";
import { pipe } from "effect";
import { Effect } from "effect";
import { Api, ApiGroup, RouterBuilder, Security } from "effect-http";
export const userApi = pipe(
ApiGroup.make("Users", {
description: "All about Users",
}),
ApiGroup.addEndpoint(
ApiGroup.get("findAllUsers", "/api/users").pipe(
Api.setRequestQuery(Criteria),
Api.setResponseBody(getPaginatedResponse(User)),
Api.setSecurity(
Security.bearer({ name: "mySecurity", description: "test" })
),
)
),
ApiGroup.addEndpoint(
ApiGroup.get("findUserById", "/api/users/:id").pipe(
Api.setRequestPath(S.Struct({ id: S.String })),
Api.setResponseBody(User),
Api.setSecurity(
Security.bearer({ name: "mySecurity", description: "test" })
),
)
),
);
const api = Api.make().pipe(Api.addGroup(userApi));
export const findAllUsers = RouterBuilder.handler(api, 'findAllUsers', ({ query }) => Effect.succeed({
data: [{ name: "mike", id: JSON.stringify(query) }],
cursor: "x",
hasMore: false,
}));
export const findUserById = RouterBuilder.handler(api, 'findUserById', ({ path }) => Effect.succeed({
name: "mike",
id: path.id,
}));
That was very helpful, thank you! I'm going to leave here the example of the approach I took in a modular way, and it's pretty nice
Ah, I was just looking for a way to modularise RouterBuilder.handler in a way that didn't require knowledge of the entire API for a single module.
Your example of:[^1]
const api = Api.make().pipe(Api.addGroup(userApi))
is an interesting (and useful!) solution. I hadn't considered that it would just accept dummy api's.
[^1]: you can add ts after the triple-backtick of a codeblock to add highlighting! :)
For the record, some time ago, I included the Handler module which enables a better modularisation. See the readme