Ok, now let's look at the source of our API, you can create each component one by one using the soap new <component_name> command, but we will use a JSON file first. Let's assume that we have planned everything before we start coding.
With the JSON file ready, we can use the command soap new --json -w to create all the elements included in this JSON. After this operation, if everything went smoothly, we should have the following file structure.
This is what the file structure looks like after creating the components. Remember, you can customize this structure by making changes to the .soap/plugin.config.json file.
After generating the files, your task will be to add logic in the controller, you will need to decide which and when use cases to invoke. In the use cases themselves, you will need to implement logic for retrieving information about all books. Since you are using a MongoDB database, you must also define how you will map values from documents to entities and vice versa. For this purpose, you need to edit the mapper file. Finally, don't forget about mapping the result obtained from the controller method and parsing it into a specific request response.
Content of the generated files
The content of the files after generation should look as follows, leaving you room for implementation. Since we didn't use the --skip-tests option, remember to fill out the test templates. By default, they are placed in the __tests__ directories.
Root layer
import { Container } from "inversify";
import bodyParser from "body-parser";
import cors from "cors";
import express from "express";
import { Routes } from "./routes";
import { Dependencies } from "./dependencies";
export * from "./routes";
export * from "./dependencies";
const start = async () => {
const app = express();
app.use(
cors({
origin: "*",
})
);
app.use(bodyParser.json());
const routes = new Routes(app);
const container = new Container();
const dependencies = new Dependencies();
dependencies.configure(container);
routes.configure(container);
app.listen(process.env.PORT, () => {
console.log(`Server is running at http://localhost:${process.env.PORT}`);
});
};
start();
import { Container } from "inversify";
import * as Soap from "@soapjs/soap";
import { GetAllBooksRoute } from "./endpoints/books/routes/get-all-books.route";
import { BooksController } from "./endpoints/books/domain/controllers/books.controller";
export class Router extends Soap.Router {
public configure(container: Container) {
const booksController =
container.get<BooksController>(BooksController.Token);
this.mount(
GetAllBooksRoute.create(booksController.getAllBooks.bind(booksController))
);
}
}
import { Container } from "inversify";
import { RepositoryImpl, ApiConfig } from "@soapjs/soap";
import { MongoSource } from "@soapjs/soap-node-mongo";
import { BooksController } from "./endpoints/books/domain/controllers";
import { GetAllBooksUseCase } from "./endpoints/books/domain/use-cases";
import { BookRepository } from "./endpoints/books/domain/repositories";
import { BookMongoMapper } from "./endpoints/books/data/mappers";
import { BookMongoCollection } from "./endpoints/books/data/collections";
export class Dependencies {
public async configure(container: Container, config: ApiConfig) {
const mongoSource = MongoSource.create(config);
await mongoSource.connect();
const context = {
collection: new BookMongoCollection(mongoSource),
mapper: new BookMongoMapper(),
queries: new MongoQueries()
}
const impl = new RepositoryImpl(context);
container
.bind<BookRepository>(BookRepository.TOKEN)
.toConstantValue(impl);
container
.bind<GetAllBooksUseCase>(GetAllBooksUseCase.TOKEN)
.to(GetAllBooksUseCase);
container.bind<BooksController>(BooksController.TOKEN).to(BooksController);
}
}
Routes
import { RouteHandler, GetRoute } from "@soapjs/soap";
import { GetAllBooksRouteIO } from "./get-all-books.route-io";
import { BooksController } from "../domain/controllers";
export class GetAllBooksRoute extends GetRoute {
public static create(handler: RouteHandler) {
return new GetAllBooksRoute("/books/list", handler);
}
private constructor(handler: RouteHandler) {
super(handler);
}
}
import { RouteIO, Response, Request } from "@soapjs/soap";
import { Book } from "../domain/entities";
import { BookRouteModel } from "./get-all-books.route-model.ts";
export class GetAllBooksRouteIO implements RouteIO {
public toResponse(output: Result<Book[]>): Response<BookRouteModel[]> {
if (output.isFailure) {
return {
body: output.failure.error.message,
status: 500
}
}
// TODO: convert result's content to the response body ...
return result.content;
}
public fromRequest(request: Request) {}
}
import { injectable } from "inversify";
import { Result } from "@soapjs/soap";
import {
Book,
GetBookInput,
AddBookInput,
RemoveBookInput
} from "./entitites";
@injectable()
export class BooksController {
public static Token = "BooksController";
public async getAllBooks(): Promise<Result<Book[]>> {
// TODO: implement method
}
public async getBook(input: GetBookInput): Promise<Result<Book[]>> {
// TODO: implement method
}
public async addBook(input: AddBookInput): Promise<Result<boolean>> {
// TODO: implement method
}
public async removeBook(input: RemoveBookInput): Promise<Result<boolean>> {
// TODO: implement method
}
}
import { injectable } from "inversify";
import { Result } from "@soapjs/soap";
import { Book } from "./entitites";
@injectable()
export class GetAllBooksUseCase implements UseCase<Book[]>{
public static Token = "GetAllBooksUseCase";
public async execute(): Promise<Result<Book[]>> {
// TODO: implement method
}
}
export class Book {
constructor(
public readonly title: string,
public readonly author: string,
public readonly publishedYear: number,
){}
}
import { injectable } from "inversify";
import { Book } from "./entitites";
@injectable()
export class BookRepository implements Repository<Book>{
public static Token = "BookRepository";
}
Data layer
import { MongoSource, MongoCollection } from "@soapjs/soap-node-mongo";
import { BookMongoModel } from "../dtos";
export class BookMongoCollection extends MongoCollection<BookMongoModel> {
constructor(source: MongoSource) {
super(source, 'book.collection');
}
}
import { Mapper } from "@soapjs/soap";
import { BookMongoModel } from "../dtos";
import { Book } from "../../domain/entities";
export class BookMongoMapper implements Mapper<Book, BookMongoModel> {
public toEntity(model:BookMongoModel): Book {
// TODO: implement method
}
public fromEntity(entity:Book): BookMongoModel {
// TODO: implement method
}
}
import { ObjectId } from 'mongodb';
export type BookMongoModel = {
id: ObjectId;
title: string;
author: string;
published_year: number;
}
This roughly outlines the generated code. Your task now will be to fill in the code segments to achieve the desired outcome when sending requests to the endpoint. Let's start with mapping the request data to the input for the controller and the controller's output to the response. All this needs to be done in the GetAllBooksRouteIO class. You can, of course, add your mapper, but essentially, RouteIO serves as a mapper itself.
In our example, the request does not contain any parameters, so there is no need to implement fromRequest. However, it's different with toResponse; the Controller always returns a Result object with the content type you choose, here an array of Book entities. We now need to convert this array into an array of BookRouteModel models or return an error if the result contains a failure.
import { RouteIO, Response, Request } from "@soapjs/soap";
import { Book } from "../domain/entities";
import { BookRouteModel } from "./get-all-books.route-model.ts";
export class GetAllBooksRouteIO implements RouteIO {
public toResponse(output: Result<Book[]>): Response<BookRouteModel[]> {
if (output.isFailure) {
return {
body: output.failure.error.message,
status: 500
};
}
return {
status: 200,
body: result.content
.map(entity => ({
author: entity.author,
title: entity.author,
published_year: entity.publishedYear
}))
};
}
}
Great, now we need to tell the controller what to do when the getAllBooks method is called. Our example is very simple and doesn't require any logic in the controller. It's your code, and you can decide whether to directly call the repository here, but for more complex scenarios, it's better to limit the controller to calling the appropriate use cases. This way, you don't clutter the controller with "helpers" or dedicated private methods (which could be untestable). The controller, by nature, takes a command and passes it on. That's what we'll do in our case, even though it's simple.
...
import { GetAllBooksUseCase } from "./use-ceses";
import { Book } from "./entitites";
@injectable()
export class BooksController {
public static Token = "BooksController";
constructor(
@inject(GetAllBooksUseCase.Token) private getAllBooksUseCase:GetAllBooksUseCase;
){}
public async getAllBooks(): Promise<Result<Book[]>> {
return this.getAllBooksUseCase.execute();
}
...
}
And now, the most important part, though in our case, it's equally easy. We need to apply the repository in the use case and call the data fetching for the books.
import { injectable } from "inversify";
import { Result } from "@soapjs/soap";
import { Book } from "./entitites";
@injectable()
export class GetAllBooksUseCase implements UseCase<Book[]>{
public static Token = "GetAllBooksUseCase";
constructor(
@inject(BookRepository.Token) private bookRepository: BookRepository
){}
public async execute(): Promise<Result<Book[]>> {
return this.bookRepository.find();
}
}
And that's all, a simple example but it gives a general idea of what you can do. Remember to complete the unit tests, create the database, and run the application.