How much time do you spend setting up the basis of a new project?
No matter how simple or complex is your service, we have to build the base, that basic base:
- Create entities
- Create the folder structure
- Then create some repositories
- Some usecases/services for each entity
- Configure the api, routes, midlewares
- etc
Come oooooooonnnn, who likes to do this?
How to save time configuring the base's project?
Today we will create a Postgres-based api with two presentation layers: Graphql and REST in less then 1 minute.
For today's dish, we use the ingredients:
To start our "fast project" we need first of all to modeling our base. Imagine we are creating an e-commerce backend app, in an e-commerce we would have 3 entities: user, product and sale.
The first step to build our project will be to create the entities using the @herbs/herbs lib that will provide some metadata to build everything else based in our entities: usecases, repositories, migrations, graphql, rest etc.
Building an entity with Herbs is very simple, just import the entity
module and pass the parameters entity("entityName", { fields })
.
All our entities will be inside a folder /entities
// entities/customer.js
const { entity, field } = require('@herbsjs/herbs')
const Customer = entity('Customer', {
id: field(Number),
cellphone: field(String),
email: field(String),
gender: field(String),
age: field(Number),
discountClub: field(Boolean),
city: field(String),
state: field(String),
address: field(String),
zipcode: field(String),
lastName: field(String),
fisrtName: field(String),
civilDocument: field(String),
})
module.exports = Customer
After create our simple entity, what about add some validations? To do this with Herbs is very simple too, just add an option object with some rules as second parameter field(Type, { options })
.
If you'd like to see more about validation types, you can check out the Herbs documentation here.
// entities/customer.js
const { entity, field } = require('@herbsjs/herbs')
const requiredString = {
validation: {
presence: true,
length: { minimum: 3, maximum: 255 },
}
}
const Customer = entity('Customer', {
id: field(Number, {
// Optional option
validation: {
presence: true,
numericality: {
greaterThan: 0,
onlyInteger: true
}
}
}),
cellphone: field(String, {
validation: {
format: /^\+[1-9]{1}[0-9]{3,14}$/
}
}),
email: field(String, {
validation: { ...requiredString.validation, email: true }
}),
gender: field(String, {
validation: { ...requiredString.validation, contains: { allowed: ["M", "W", "X"] } }
}),
age: field(Number),
discountClub: field(Boolean),
city: field(String, requiredString),
state: field(String, requiredString),
address: field(String, requiredString),
zipcode: field(String, requiredString),
lastName: field(String, requiredString),
fisrtName: field(String, requiredString),
civilDocument: field(String, requiredString),
})
module.exports = Customer
// entities/product.js
const { entity, field } = require('@herbsjs/herbs')
const required = { validation: { presence: true } }
const requiredString = {
validation: {
...required.validation,
length: { minimum: 3, maximum: 255 },
}
}
module.exports = entity('Product', {
id: field(Number, {
// Optional option
validation: {
presence: true,
numericality: {
greaterThan: 0,
onlyInteger: true
}
}
}),
limitDiscount: field(Number, {
validation: {
greaterThan: 0,
lessThan: 100,
onlyInteger: true
}
}),
size: field(String, required),
stock: field(Number, required),
color: field(String, required),
name: field(String, requiredString),
brand: field(String, requiredString),
category: field(String, requiredString),
description: field(String, requiredString),
price: field(Number, { validation: { presence: true, greaterThan: 0 } }),
})
// entities/sale.js
const { entity, field } = require('@herbsjs/herbs')
const required = { validation: { presence: true } }
module.exports = entity('Sale', {
id: field(Number, {
// Optional option
validation: {
presence: true,
numericality: {
greaterThan: 0,
onlyInteger: true
}
}
}),
productID: field(Number, required),
customerID: field(Number, required),
total: field(Number, required),
quantity: field(Number, required)
})
In the end we will have this result:
generating the project
Finally our dreamed come to be true, where we generated our project based on our entities!
The Herbs CLI was created to help you create and maintain your project in a faster way when you uses the entire Herbs ecosystem.
When we type herbs new
herbs will ask a some things about your project to generate our project.
Note the last question is an optional question, if you don't specify your entities folder path, the project will be generated based on a default “User” entity, but we're not here to generate a “boilerplate” are we? So let's fill it in with out entities folder path.
When you press Enter our app will be generated and you will find a folder with the project name.
look this! How much automatically generated stuff! How much time we saved here! Very satisfying! Note that the entire project is done: migrations, repositories, api(graphl and rest) create, read, update and delete usecases and everything else!
Explaining the project
Okay, but what we built here?
The diagram below shows a little bit how the project is organized (which you can check on the Herbs doc home), the generated app is based on the clean architecture, there we have our domain
with our entities
and our usecases
, around from this we have the rest of our application (api, database, clients, documentation, etc.) and in all these other layers use the herbsjs "glues" that read the entities and usecases metadatas to facilitate our day-a-day software development, remembering that no glue is a code generator
, are just tools to speed up the development of your code.
now let's be a little practical and talk about the our project's folder structure.
NOTE: remembering that architecture is not a folder structure!
Inside domain
folder we have our entities and usecases that provide some metadata as usecase's name, request and output schema, entity fields, types and more.
Above we have herbs2gql glue example, the herbs2gql receive a usecase and a resolver so it return the Graphql schemas string.
The concept of “glue” runs throughout the application, from the presentation layer to the repository, remembering it isn't mandatory and this libraries are only meant to help you develop and maintain your project. :)
Inside our usecases we can see that exists not anly a simples creation, but there have a validations too, so it means the generated project is done to production!
Running the project
Once we've generated and understood the code, let's put this code to work! Just give the npm run start
command and watch the magic happen. Note that all REST routes are printed on the terminal and that they are all based on our usecases.
Also, if you open your browser on the http://localhost:3000/graphql
route you will see all generated schemas for the Graphql presentation layer with the Graphql playground documentation
Beside everything we generated here, the herbs generates a dynamic documentation too, so imagine you'll never need to worry about your API's documentation since everything will be automatically generated via usecase metadata and your Readme.md file! To do that we use the HerbsShelf glue
So now just implement your business rule and go up to production!
Top comments (0)