Skip to main content

4. Creating Use Cases

Introduction to the Use Case concept

A use case reflects a single action exposed by the Domain to the end user.

For exemple: Reopen Ticket, Reply Message, Add User

Internally, a use case is responsible for controlling the interaction between entities, repositories and other domain components.

With the entities set properly, we can start to use them. For this, we have to set the use cases for the app.

We are going to set use cases to interact with the User entity created in the previous step.

Learn more: What's a Use Case?

Create User

Let's understand the use case to create a user that is generated by the CLI. For that, we are going to walk through the following topics:

  • Use Case Name
  • Request
  • Response
  • Setup / DI
  • Authorize
  • Steps - Basic
  • Step return (Ok, Err)
  • Use Case return (ctx.ret)

Learn more about create usecases.

Use Case Name

First, the name is set for the usecase.

// src/domain/usecases/user/createUser.js
const { usecase } = require('@herbsjs/herbs')

const createUser = () => usecase('Create User', {})

Request

Now, we have to specify what are the parameters accepted from the user on request.

In this case, we need user's nickname and password, which are both String since we want a text.

Here we can use any other Object type, like: Boolean, Number, etc. To set up an array, we have to add brackets around the type, like: [String].

// src/domain/usecases/user/createUser.js
const { usecase } = require('@herbsjs/herbs')

const createUser = () => usecase('Create User', {
// Input/Request metadata and validation
request: {
nickname: String,
password: String
},
})

If we were using an Entity in the request field, the validations set in the Entity will also be checked there.

Response

Once we have the request object specified, we must specify the response model.

If your use case does not need to return a response and just perform operations, you can ommit this field.

In this case, it will return the User Entity.

// src/domain/usecases/user/createUser.js
const { usecase } = require('@herbsjs/herbs')
const User = require('../entities/user');

const createUser = () => usecase('Create User', {
// Input/Request metadata and validation
request: {
nickname: String,
password: String
},

// Output/Response metadata
response: User,
})

Setup / DI

The use case is divided by steps, they run one-by-one and can share a context object. You are free to use this object setting useful values between the steps. Besides that, you can also have an initial object setup, where you can set initially required values for steps, like repositories (it's what makes possible the interaction with the database).

In each use case step (which we are going to set soon), a context object is provided to handle the "data sharing" between different steps. And other useful data, such as repositories, the request values, etc.

On the setup function, we can manually add values to this context, so we can use them later in the steps.

In this case of the CLI, it was not necessary, but wel could do something like this:

// src/domain/usecases/user/createUser.js
const { usecase } = require('@herbsjs/herbs')
const User = require('../entities/user');

const createUser = () => usecase('Create User', {
// Input/Request metadata and validation
request: {
nickname: String,
password: String
},

// Output/Response metadata
response: User,

// Pre-run setup
setup: ctx => {
ctx.myCustomData = "any useful data";
},
})

Authorize

Use cases may also have an authorize function, which can be implemented with any logic and must return Ok() if user is authorized to perform that operation and Err() otherwise.

The authorize function is runned before any use case. Use cases will only run if the user is authorized.

The CLI default function just allows all requests by constant returning Ok(), but you can implement any logic for that. For example, we can suppose that the user object has a property called canCreateUser.

Learn more: Use Cases Features - Authorize

// src/domain/usecases/user/createUser.js
const { Ok, Err, usecase } = require('@herbsjs/herbs')
const User = require('../entities/user');

const createUser = () => usecase('Create User', {
// Input/Request metadata and validation
request: {
nickname: String,
password: String
},

// Output/Response metadata
response: User,

// Authorization with Audit
authorize: async (user) => (user.canCreateUser ? Ok() : Err()),
})

Steps

Since we have the request, response, setup and authorization set, we can finally start writing the actual logic in the steps.

Steps are the building blocks of a use case. Their main goal is to generate metadata before and during use case execution like the code intention, audit trail, etc. The first thing to note is that we encourage steps description with the business intent (never the technical intent).

Learn more about usecase steps.

// src/domain/usecases/user/createUser.js
const { Ok, Err, usecase } = require('@herbsjs/herbs')
const User = require('../entities/user');

// Here, we receive an object with the `userRepository` in it.
// So we can interact with the database.
const createUser = ({ userRepository }) => usecase('Create User', {
// Input/Request metadata and validation
request: {
nickname: String,
password: String
},

// Output/Response metadata
response: User,

// Authorization with Audit
authorize: async (user) => (user.canCreateUser ? Ok() : Err()),

// Step description and function
'Check if the User is valid': step(ctx => {
// Creates a new user from the request.
// And stores it in the context.
ctx.user = User.fromJSON(ctx.req)

// Check if the fields are valid.
if (!ctx.user.isValid())
return UserNotValidError('User ', 'The User entity is invalid', ctx.user.errors)

// returning Ok continues to the next step. Err stops the use case execution.
return Ok()
}),

'Save the User': step(async ctx => {
// Insert the new user to the repository
// and then return it to the client using
// the `ret` property of context object.
return (ctx.ret = await userRepository.insert(ctx.user))
})
})