efesto
TypeScript icon, indicating that this package has built-in type declarations

0.15.1 • Public • Published

Efesto

A fast, easy and powerful framework for API costruction with node and express


Main Features:

Efesto provides a large amount of facilitations like:

  • Easy and clean way to write API documentation throughout swagger
  • Easy way to validate requests (body, headers etc...)
  • Enable caching throughout redis
  • Easy way to authenticate your endpoints
  • Support for ABAC
  • Easy error handling
  • Enhance your endpoints structure only with the file/folder positioning
  • Full TypeScript support

Main Dependencies:

Installation and Setup:

Install the efesto package into your project using:

npm i efesto@tridente-neptuni

in your app file use efesto as a middleware:

  ...
  import efesto from "efesto"

  const app = express();

  app.use("/api/v1", efesto(efestoConfiguration))

efesto configuration object is structured like this:

Parameter Required Description Default
authMiddleware the authentication middleware - read more /
errorMiddleware the middleware which handles errors - read more /
isProduction Enables the production mode: the watch will be on pointes js (dist) files /
options Efesto's options object undefined

Options

Parameter Description Default
absoluteDirRoutes Path where Efesto will find the files to parse "/v1/routes"
relativeDirSwaggerDeclarationsPath Folder where Efesto will create the swagger .yaml files "swagger-declarations"
config The Efesto's configuration object undefined

Config Object

Parameter type Description Default
createdAtField string Define the field used as createdAt timestamp "createdAt"
updatedAtField string Define the field used as updatedAt timestamp "updatedAt"
multer MulterConfigurationObject The multer configuration object undefined
dynamicParameterType string The path dynamic parameters type "string"
defaultRequiredValueForModels boolean true if all the parameters are required by default false
abacPermissions AbacConfigurationObject ABAC configuration object undefined
redis redisConfigurationObject redis configuration object undefined
optionalFieldType string "undefined" or "null", the type optional field should have (needed if using Efesto with Prisma) "undefined"

Configuration

The efesto configuration is pretty easy:

You just need to create a baseIndex.json file with the base configuration for swagger (servers, authentication, ecc...) in this file you can also specify general schemas

💭 In order to generate a Swagger UI we suggest you use @apidevtools/swagger-parser bundling all the declarations in an unique .json file and, using swagger-ui-express, generate the swagger UI

Authentication Middleware

The authentication middleware it's just a simple middleware that must be given to Efesto in order to authenticate all the requests, if you do not want to authenticate the requests declare a function like this:

(req: Request, res: Response, next: NextFunction) => {
  next();
};

In your authentication middleware you can do whatever you want, remember that whatever you put inside the request can be retrieved in all the efesto middlewares

Error Middleware

The error middleware it's a middleware where you can handle all the errors and exceptions that must be given to Efesto.

If you don't have any error to handle you can simply pass a function like this:

(req: Request, res: Response, next: NextFunction) => {
  next();
};

Usage

ℹ️ The following explanation assumes that you set "swagger-declarations" as relativeDirSwaggerDeclarationsPath, "/v1/routes" as absoluteDirRoutes and declared, on express, "/api/v1" as main path for Efesto

Declaring endpoints

In order to declare an Efesto's endpoint you just need to create a file inside the "/v1/routes" directory so, creating an index.ts or index.js (depending on your isProduction configuration value) in /v1/routes/users will automatically generate the path /api/v1/users on your server.

In the same way creating an anyname.ts file in the same folder will result in the generation of the path /api/v1/users/anyname

Declaring methods

this is an Efesto's file example:

class ModelName extends BaseApiService {
  constructor() {
    super(__filename);
  }

  swaggerModel: SwaggerModel = {
    modelName: "ModelReference",
    schemas: [
      {
        name: "User",
        properties: {
          id: "number",
          name: "string",
          surname: "string",
        },
      },
    ],
  };

  _getSwagger: SwaggerOptions = {
    operationId: "getUsers",
    cache: {
      key: "`users`",
      expiresInSeconds: 600,
    },
    responses: {
      200: {
        content: {
          "application/json": {
            schema: {
              type: "array",
              items: "@User",
            },
          },
        },
      },
    },
  };

  async _get(req: express.Request, res: express.Response, next: express.NextFunction) {
    return res.sendStatus(200);
  }
}
export default ModelName;

Now let's analyze the example above in code chunks in order to understand the structure of the file:

First of all the file must export by default a class which extends BaseApiService (the class name does not matter) having as super() constructor function the file name (you can access it throughout the NodeJs variable __filename)

class ModelName extends BaseApiService {

  constructor(){
    super(__filename);
  }

  ...

}
export default ModelName

The swaggerModel property inside the class is used to specify the "category" (for example if the endpoint contains methods to control users the best "category" may be "users" or "user") of the endpoint which have to be specified in the modelName attribute of swaggerModel. Specify the modelName is highly recommended as the absence of this attribute will result in grouping all the endpoints under the n-a category.

In the swaggerModel property can also be specified swagger models, the peculiarity of those models (as all the other Efesto's models) is that, unlike in swagger, models are shared in all the Efesto project. All the models must be specified in the schemas attribute of swaggerModel

class ModelName extends BaseApiService {

  ...

  swaggerModel: SwaggerModel = {
    modelName: "ModelReference",
    schemas: [
      {
        name: "User",
        properties: {
          id: {
            type: "number"
          },
          name: {
            type: "string"
          },
          surname: {
            type: "string"
          },
        },
      },
    ],
  };

  ...

}
export default ModelName

Every method (POST, PUT, DELETE, etc...) must have both a swagger declaration (_<method>Swagger) and a corresponding function (can be asynchronous or synchronous) (_<method>(req, res, next)) this function will be the middleware that will be triggered when a request will be done at the endpoint using, as method, <method>.

class ModelName extends BaseApiService {

  ...

    _getSwagger: SwaggerOptions = { // swagger declaration for GET method
    operationId: "getUsers",
    responses: {
      200: {
        content: {
          "application/json": {
            schema: {
              type: "array",
              items: "@User",
            },
          },
        },
      },
    },
  };

  async _get(req: express.Request, res: express.Response, next: express.NextFunction) { // middleware for GET method
    return res.sendStatus(200);
  }

  ...

}
export default ModelName

As you can see in the above snippet, using the @ notation (@<schemaName>) will result in the automatic binding and parsing between the declared schema reference and the actual declaration path in the .yaml file.

This will be the declaration inside the .yaml file:

$ref: index.yaml#/components/schemas/User

Dynamic paths

Efesto supports dynamic endpoints throughout parameters in paths:

for example, having this file structure:

v1/
├─ routes/
   ├─ users/
      ├─ [userId].ts

will result in a path like this: /api/v1/users/[userId] where userId can be a string or number depending on the dynamicParameterType value. This means that, in the [userId] path request.params will have userId

eg:

if someone contacts your server using this route: api/v1/users/620d1be516cd481f1131169d

class ModelName extends BaseApiService {
  async _get(req: express.Request, res: express.Response, next: express.NextFunction) {
    console.log(req.params.userId); // 620d1be516cd481f1131169d
    return res.sendStatus(200);
  }
}
export default ModelName;

the value of req.params.userId will be 620d1be516cd481f1131169d

Request Validation

Efesto uses express-validator for validate your requests:

you can declare the validation inside the Efesto's files:

class ModelName extends BaseApiService {

  ...

   _putValidation = [check("name").isString()]

  ...

}
export default ModelName

in this example the validation will search in the request if name is present and it's a string, for further information (also about the error handling) read the express-validator documentation.

Overriding Authentication

Due to the fact that not every endpoint requires the same authentication you can override the default authentication in every method:

class ModelName extends BaseApiService {

  ...

   _getOverrideAuth = (req: Request, res: Response, next: NextFunction)=> {
     next()
   }

  ...

}
export default ModelName

in this case the GET endpoint authentication will be overrode by the new middleware

Using permissions (ABAC)

If you are using ABAC in your project you can specify the general permission in the swagger specification. This will not only be written in the swagger files but will be also checked before executing requests.

Be sure to configure correctly Efesto adding inside the config object the required data in order to make the permissions handling work properly

The required data is contained in the ABAC configuration object:

ABAC configuration object

the ABAC configuration object is structured as follows:

property type description default
actions string[] all the available actions
models string[] all the available models
checkPermissionBeforeResolver boolean if true check permissions before executing the endpoint method true
reqAbilityField string the request field where efesto can find the permission object ability

Once you have defined all the properties you will be able to use ABAC in Efesto.

ABAC usage

ℹ️ The following instruction are written assuming that the reqAbilityField parameter is set to ability. Furthermore in order to make the ABAC work correctly the use of the @casl/ability library is highly recommended

First of all, in your authentication middleware, (or in any other middleware) create the abilities

This is how you can declare required permission:

class ModelName extends BaseApiService {

  ...

    _getSwagger: SwaggerOptions = {
    operationId: "getUsers",
    permission: ["readAll", "Users"],
    responses: {
      200: {
        content: {
          "application/json": {
            schema: {
              type: "array",
              items: "@User",
            },
          },
        },
      },
    },
  };

  ...

}
export default ModelName

in this case, before executing the method Efesto will check if the client has the required permission. If not a ForbiddenError will be raised.

Using Cache

Efesto has an embedded caching system based on Redis. In order to enable this feature you need to configure Redis when instancing Efesto.

this.app.use(
      "/api/v1",
      efesto({
        ...
        options: {
          ...
          config: {
            redis: { // You need to declare this object in the configuration
              host: "127.0.0.1" // Redis IP/endpoint,
              port: 9091 // Redis port,
              defaultExpiresInSeconds: 600 // Redis data default Time To Live,
            },
          },
        },
      })
    );

Once you declared this data Efesto's caching system is ready to go!

In order to cache an endpoint response you just need to declare the corresponding Redis key:

_getSwagger: SwaggerOptions = {
    ...
    cache: {
      key: "`users`",
    },
    ...
  };

Remember that the key attribute must contain a string that has to be evaluated so using backticks in it ("``") is required.

Furthermore the key has access to the req object (the request) so you can create a dynamic key based on the request data.

Assuming that you have inside your request an user object composed like this:

{
  id: "622b8386f96d33fa0c24428b",
  name: "Foo",
  surname: "Bar"
}

In order to have a cache based on the user in the request you will have to describe the key like this:

_getSwagger: SwaggerOptions = {
    ...
    cache: {
      key: "`user-${req.user.id}-operationid`",
    },
    ...
  };

that will result (in this specific case) in the following key: user-622b8386f96d33fa0c24428b-operationid.

If the cached data for the endpoint is found in redis Efesto will skip the method returning the cached data. you can also specify the specific TTL for the endpoint cache:

_getSwagger: SwaggerOptions = {
    ...
    cache: {
      key: "`user-${req.user.id}-operationid`",
      expiresInSeconds: 60 // this cached data will last 1 minute
    },
    ...
  };

In order to purge the cache you have to specify which endpoint invalidates the cached data using the purge attribute:

_postSwagger: SwaggerOptions = {
    ...
    purge: ["`users`"]
    ...
  };

the purge attribute accepts an array of Redis keys that will be deleted on the endpoint call.

Both key and purge parameters are optional and support all Redis operators

Uploading files

In case you need to upload files throughout your APIs Efesto uses multer to handle file uploads.

In order to upload files you have to declare the content of the request body as "multipart/form-data" and enable the multer flag:

class ModelName extends BaseApiService {
  ...
  _postSwagger: SwaggerOptions = {
    ...
    requestBody:{
      content:{
        "multipart/form-data":{
          schema:{
            type: "object",
            properties:{
              file: {
                type: "string",
                format: "binary"
              }
            }
          }
        }
      }
    }
  };

  _postMulter: boolean = true;

  ...
}

using this configuration the file will be accessible in the request:

async _post(req: Request, res: Response, next: NextFunction){

  const file = req.file // this will be the uploaded file
  ...
}

Efesto accepts by default single file uploads, in order to enable the ability of upload multiple files at once you need to set the property MultipleMulter in the endpoint declaration:

class ModelName extends BaseApiService {
  ...
  _postSwagger: SwaggerOptions = {
    ...
    requestBody:{
      content:{
        "multipart/form-data":{
          schema:{
            type: "object",
            properties:{
              files: {
                type: "array",
                items:{
                  type: "string",
                  format: "binary"
                }
              }
            }
          }
        }
      }
    }
  };

  _postMulter: boolean = true;
  _postMultipleMulter: boolean = true // required for multiple file uploads

  ...
}

using this configuration the files will be accessible in the request:

async _post(req: Request, res: Response, next: NextFunction){

  const files = req.files // this will be the array of uploaded files
  ...
}

Shorthands

Writing documentation in Swagger can sometimes be redundant and confusing. For this reason Efesto has a bunch of shorthands that can lighten up the documentation writing:

  • Schemas reference and sharing | @Schema

    As told before Efesto, unlike swagger, shares all the declared schemas in the whole project and can be accessible using the @ followed by the schema name.

    ⚠️ This is also the suggested way to refer to a schema due to the fact that, otherwise, you will need to specify in the $ref property the .yaml generated file path using the Swagger syntax

    Example:

    class ModelName extends BaseApiService {
      constructor() {
        super(__filename);
      }
    
      swaggerModel: SwaggerModel = {
        modelName: "ModelReference",
        schemas: [
          {
            name: "User",
            properties: {
              id: {
                type:"number"
              },
              name: {
                type:"string"
              },
              surname: {
                type:"string"
              },
            },
          },
        ],
      };
    
      _postSwagger: SwaggerOptions = {
        ...
        responses:{
          200: {
            content:{
              "application/json":{
                schema:"@User"
              }
            }
          }
        }
      }
    }
  • Formats | type::format

    In order to declare a format you can use the following syntax:

      ...
      properties: {
        date: {
          type: "string::date-time"
        },
      },
      ...
  • Examples | type|example

    In case you need to declare an example you can easily declare with this syntax:

      ...
      properties: {
        name: {
          type: "string|Joe"
        },
      },
      ...

    ℹ️ This last two shorthands (format and type) are chainable together

  • Quick Type Declaration | propertyName: "type"

    In the documentation each property must have a declared type and it's rare that this type will have a format or an example associated with it.

    OpenAPI documentation forces you to declare a type property for each attribute you add to the documentation making it pretty redundant. That's why Efesto introduces also a shorthand to quickly declare a property type so:

      ...
      properties: {
        name: {
          type: "string"
        },
      },
      ...

    can be declared as:

      ...
      properties: {
        name: "string"
      },
      ...
  • Quick Content Declaration | requestBody: "type|@Schema" / <statusCode>: "type|@Schema"

    Each endpoint has a also a corresponding response and, sometimes also a request body; declaring them as required in the OpenAPI specification can affect your code cleanliness. Assuming that you are writing a login endpoint using the standard OpenAPI specification format you should end up with a snippet like this:

    class ModelName extends BaseApiService {
      constructor() {
        super(__filename);
      }
    
      swaggerModel: SwaggerModel = {
        modelName: "ModelReference",
        schemas: [
          {
            name: "User",
            properties: {
              id: {
                type:"number"
              },
              name: {
                type:"string"
              },
              surname: {
                type:"string"
              },
            },
          },
          {
            name: "UserLoginRequestBody",
            properties:{
              username: {
                type: "string"
              },
              password:{
                type: "string"
              }
            }
          }
        ],
      };
    
    _postSwagger: SwaggerOptions = {
      operationId: "login",
      requestBody:{
        content:{
          "application/json":{
            schema: "@UserLoginRequestBody"
          }
        }
      }
      responses: {
        200: {
          content: {
            "application/json": {
              schema: "@User",
            },
          },
        },
      },
    };
    
    }

    just applying the above documented shorthands the code can be cleaned a little bit like this:

    class ModelName extends BaseApiService {
      constructor() {
        super(__filename);
      }
    
      swaggerModel: SwaggerModel = {
        modelName: "ModelReference",
        schemas: [
          {
            name: "User",
            properties: {
              id: "number",
              name: "string",
              surname: "string",
            },
          },
          {
            name: "UserLoginRequestBody",
            properties:{
              username: "string",
              password:"string"
            }
          }
        ],
      };
    
      _postSwagger: SwaggerOptions = {
        operationId: "login",
        requestBody:{
          content:{
            "application/json":{
              schema: "@UserLoginRequestBody"
            }
          }
        }
        responses: {
          200: {
            content: {
              "application/json": {
                schema: "@User",
              },
            },
          },
        },
      };
    
    }

    but as you can see the request body and the response are extremely long and could be written in a more efficient way:

    class ModelName extends BaseApiService {
      constructor() {
        super(__filename);
      }
    
      swaggerModel: SwaggerModel = {
        modelName: "ModelReference",
        schemas: [
          {
            name: "User",
            properties: {
              id: "number",
              name: "string",
              surname: "string",
            },
          },
          {
            name: "UserLoginRequestBody",
            properties:{
              username: "string",
              password:"string"
            }
          }
        ],
      };
    
      _postSwagger: SwaggerOptions = {
        operationId: "login",
        requestBody: "@UserLoginRequestBody"
        responses: {
          200: "@User"
        },
      };
    
    }

    in this way the verbose response declaration can be skipped and the response will be by default "application/json" and will have as response content (in this specific case) an object of type User. This will clean up your code and will help you to make it more readable and understandable.

    ℹ️ Note that this shorthand can also be used inside the schema declaration of a parameter:

     _postSwagger: SwaggerOptions = {
       operationId: "login",
       parameters: [
        {in: "query", name: "exampleParam", schema: "string" }
       ]
       requestBody: "@UserLoginRequestBody"
       responses: {
         200: "@User"
       },
     };

    instead of:

     _postSwagger: SwaggerOptions = {
       operationId: "login",
       parameters: [
        {in: "query", name: "exampleParam", schema: {type: "string"} }
       ]
       requestBody: "@UserLoginRequestBody"
       responses: {
         200: "@User"
       },
     };

With this information you should now be ready to go using Efesto!

Have a nice forging! ❤️

Readme

Keywords

none

Package Sidebar

Install

npm i efesto

Weekly Downloads

88

Version

0.15.1

License

ISC

Unpacked Size

169 kB

Total Files

29

Last publish

Collaborators

  • mabiloft