NGN

A set of utils built for NextJs to make developers’ job easier.

NextJs is a super powerful framework developed by javascript, but the API part
is too raw comparing to other frameworks built on PHP for example. So I’ve
developed a set of utils for it and called it NGNJs.

Concepts

Milkshake Commander

Milkshake is a cli solution that helps developers to focus on what they
want to develop and reduce their errors. It can do many things such as

  • Initiating NGN
  • Make Pages, Apis, etc.
  • Generate APP_KEY for AES encryption

You will become more familiar with Milkshake in later sections.

Errors

When using NGN, Every problem throws an error. Errors are caught by NGN api
handler, and its message is returned with appropriate status code. You will
learn more about errors and how to set their status codes later.

Facades

Some useful facades is available in NGNJs. Some of them brings new functionality
to NextJs, and some other just make works done easier.

Middlewares

About 10 middlewares are available out of the box, and almost all of them are
used globally.

Services

In-app Caching, Logging, request validation and resources are services
implemented in NGNJs. All of them will be described in details in later
sections.

Request handler

All API requests is handled using NGNJs request handler. This handler uses
next-connect and its powerful
features to handle requests.

Now that you know what NGN is, it’s time to install it!

Install NGNJs package

Simply rum npm i --save ngnjs if you’re using NPM and yarn add ngnjs if you
are a yarn user. After installation, milkshake binary file will be added to
node_modules/.bin directory. So you can use it simply by calling it. NGNJs
needs a set of files and configurations to work properly and let you customize
NGNJs functionality. By calling milkshake init, All of these files will be
created for you. These files are:

  • config directory: All NGNJs config files are located in this directory.
    You can add or remove middlewares, set middlewares to a group of routes,
    change throttling duration and number of requests, add some trusted proxy
    ips if you’re proxying your code.
  • storage directory: All files created by caching systems, all files you
    upload and all of your log files are located at this directory. You will
    learn how to upload files, log a message and use caching system later.
  • .env.example and .env.local: Some environment variables need to be set
    so NGNJs can read and use them. All these variables are listed in
    .env.example file. This file is a sample file for you to know what
    variables are necessary for NGNJs. In the other side, .env.local is
    real .env file and has real values for variables. So it’s git ignored.
    Be careful to back up you .env.local and .env.example file before using
    milkshake init. Milkshake will override env files
  • handler.ts: It’s NGNJs request handler for API requests.
  • pages/api/storage/[…path].ts: This endpoint is used for pseudo static file
    serving.
  • pages/api/log.ts: Used to log a message in log files from clientSide.

That’s it! You’re all set! now lets check how to use NGNJs features

Milkshake

Milkshake is a cli script for making files from templates and generating APP_KEY
(APP_KEY is used for encryption). Supported commands are described bellow:

init

This command initiates NGNJs for use. What is created during initialization is
described in installation section.

make

This command makes a file from template. For now, templates are:

  1. api: milkshake make api asks you some simple questions and makes an api
    endpoint for you. base path of api is /pages/api and you can give milkshake
    a path as api name to make your endpoint file in a sub path. For example if you
    choose your api name as users/index, an api file called index is created in
    pages/api/users/ directory.
  2. middleware: milkshake make middleware asks you the name of your middleware
    and creates a middleware file in src/middlewares/ directory. Don’t worry if you
    don’t have middlewares directory. It will be created when needed. You can give
    a path as name to create this file in a sub directory under src/middlewares/.
  3. page: milkshake make page asks you some questions and makes a page template
    for you under pages/ directory. You can give a path as name to create this
    file in a sub directory under pages/.
  4. resource: milkshake make resource creates a resource file for you under
    src/resources. Resources will be described in later sections
  5. rule: milkshake make rule will create a custom validation rule under
    src/validationRules. Validation rules will be described in later sections.

keygen

This command creates a new key for your application and sets it in .env.local
file.

Requests and responses

If all NGNJs middlewares are globally applied as default config, You have some
more features than default NextJs:

  1. req.files: This key is only available if your api endpoint supports
    form-data. Supporting form-data is asked from you when creating an endpoint
    using milkshake make api command. All uploaded files using form data is
    stored in this key. You can upload these files by simply calling upload
    method of one of files. This method has two args:
  • path: a string which determines the destination file path under
    storage/public/uploads/ when private mode is false and storage/ when private
    mode is true.
  • isPrivate: a boolean value which determines private mode. In the other words,
    setting isPrivate to true will upload your file in selected path under
    storage/ and setting it to false will put your file under
    storage/public/uploads.
  1. req.session: A server-side session storage. You can set value to session by
    simply assign a value to a key of req.session. For example imagine you want
    to set a session key called name with the value of NGN. You can do it in
    this way: req.session.name = "NGN". Reading data from session is as simple
    as reading the value of that key of session. For example getting name key
    will be done this way: const name = req.session.name;

  2. req.all: req.query, req.files and req.body are merged in req.all.
    It’s useful when you don’t know if request parameter is passed in query
    string, or body, or it’s a file.

  3. req.validate: It’s an async function which validates your api inputs
    using rules. Using this feature is something like this:

    Imagine we have an endpoint which has name field as required field.
    There is no need to check it in your api logic. All you need to do is:

    await req.validate({
    	"name": [new RequiredRule()]
    });

    This code snippet checks if name field is passed to your api. If it’s
    not passed, a 422 status code will be returned with the error of invalidated
    field. For example if name is not passed to the code above, something
    like the bellow response will be returned:

    {
       "message": "The given data is invalid",
       "errors": {
           "name": "Field is required"
       }
    }

    You can pass as many rules as you want to a key since value of validate
    argument MUST be an array. There are some rules available
    out-of-the-box: EmailRule for regex email validation, InRule for
    checking if given key exists in an array, NumberRule to check if
    input is a number, RequiredRule and StringRule.

    You can implement your own rule by creating a rule using
    milkshake make rule and implementing your logic in it.

    Note: Validator can also be used in client-side. You will learn it
    in next sections.

  4. req.ip: returns IP of client. If you’re serving application using a proxy,
    You need to set your proxy ip to config/trustedProxies.json file in order
    to read X-Real-Ip or X-Forwarded-For to get ip address, otherwise
    remote address will be returned.

  5. res.setCookie: This function encrypts cookie value and sets it.

    Note: Since it’s possible for a website to have cookies which
    set from other packages, NGNJs adds a prefix to your cookie name to
    find out which cookies need decryption. Don’t worry. This cookie name
    prefix will be deleted when cookies are decrypted and passed to
    req.cookies. You only need to use prefixed cookie name if you want to
    access to a cookie client-side. This method is chainable, and you can use
    it as many times as you want. You can customise cookie prefix in .env.local
    by changing COOKIE_PREFIX value.

Validation

Form validation can be done in both client and server side using validation
rules. Validation rules are some javascript classes which must have two
methods implemented:

  1. public async evaluate(object): Has only one argument which is the value
    of the key you mapped to a rule. This value is passed to validation rule
    class automatically when it’s called. Your validation logic should be
    implemented in this method and returns true on validation success and
    false otherwise.

  2. public getError(fieldName): fieldName mapped to a rule will be passed
    automatically to this method. This method should return a message which
    contains error information on validation error.

You can also implement the constructor method if you want to pass some data
to your validator class.

There are some rules implemented in NGNJs out-of-the-box:

  • EmailRule: Validates an email address using regex.
  • InRule: Validates if value under validation is existed in an array.
  • NumberRule: Checks if value is a number
  • RequiredRule: Checks if value is existed.
  • StringRule: Checks if value type is string.

An example of implementing a rule:

You can create a custom rule using milkshake make rule. the code
bellow is InRule which is implemented in NGNJs:

typescript:

import Rule from "ngn/dist/services/validator/rules/Rule";

class InRule implements Rule {
   private readonly arr: Array<string|number>;
   constructor(arr: Array<string|number>) {
      this.arr = arr;
   }
   evaluate(object: any): boolean {
      return ((!object && typeof object !== "boolean") || this.arr.includes(object));
   }

   getError(fieldName: string): string {
      const arrayString = JSON.stringify(this.arr);
      return `${fieldName} must be in ${arrayString}`;
   }
}

export default InRule;

javascript:

class InRule{
    arr;
    constructor(arr) {
        this.arr = arr;
    }
    evaluate(object) {
        return ((!object && typeof object !== "boolean") || this.arr.includes(object));
    }

    getError(fieldName) {
        const arrayString = JSON.stringify(this.arr);
        return `${fieldName} must be in ${arrayString}`;
    }
}

export default InRule;

Using validators in pages

Using validation in pages (both CSR and SSR) is done using validate function. This function gets two args:

  • An object maps field names to an array of rules as the first argument.
  • The object you want to validate as the second argument.

validate function returns an object with two keys:

  • result: a boolean value which is true on validation success and false otherwise
  • errors: an object of errors which maps field names to errors.

For example imagine you have a form which creates a key in a state on input changes:

import {useState} from "react";
import validate from "ngn/dist/services/validator/validate";
import EmailRule from "ngn/dist/services/validator/rules/EmailRule";
import RequiredRule from "ngn/dist/services/validator/rules/RequiredRule";

const Form = () => {
    const [data, setData] = useState({});
    const fillData = (e) => setData(state => ({...state, [e.target.name]: e.target.value}));
    const validateForm = (e) => {
        e.preventDefault();
        // We will implement validate method later
    } 
    
    return (
        <form onSubmit={validateForm}>
           <input type="text" name="name" placeholder="name" onChange={fillData}/><br/>
           <input type="text" name="surname" placeholder="surname" onChange={fillData}/><br/>
           <input type="text" name="email" placeholder="email address" onChange={fillData}/><br/>
           <button type="submit">Send</button>
        </form>
    )
}

Now we have something like bellow in data variable after filling form:

{
   "name": "Farbod",
   "surname": "Shams",
   "email": "[email protected]"
}

So want we want to validate is this object. We can use validate asynchronous function like this:

const validateForm = (e) => {
    e.preventDefault();
    validate({
       name: [new RequiredRule()],
       surname: [new RequiredRule()],
       email: [new RequiredRule(), new EmailRule()],
    }).then(res => {
        if(!res.result)
            console.error("Validation error: ", errors)
       else {
           //Do main thing
        }
    })
} 

Using validators in API

In api you can simply use req.validate asynchronous function. Is has only one argument which is an object maps field
names to an array of rules. All of your request data (query string, body and files) are passed to validator function
automatically. req.validate throws an ValidationError error on request is not validate with rules check. This error
is automatically caught and returns an object holds message and errors with the status code of 422.

Errors

Almost all API errors are caught using NGN request handler. There are two
main types of errors:

  1. NGNError: THis error is a throwable class with extended from javascript
    native Error class. NGNError expects three arguments:

    • status: status code of response when error is thrown.
    • message: error message
    • context: an object which holds additional data about error and
      returned in response.

    Feel free to develop your own errors with even default status code or
    message by extending this class.

  2. NGNSilentError: Is NGNError but error message and context is shown
    to user only when APP_DEBUG env variable is set to true and
    Server error message will be returned otherwise. You always have
    error details in logs by the way.

Logging

Logging is NGNJs is as simple as importing Logger class. Log files are
located in storage/logs/ directory. new log file is generated each day and
older log files are never deleted. Logs can be submitted in 8 different
levels: (emergency, alert, critical, error, warning, notice, info, debug)
These levels are the priority of log message. You can set LOG_LEVEL env
variable to ignore logs with levels less than LOG_LEVEL to be logged.

logging in server-side

Since logging required filesystem support, you can use Logger class only
in server-side. This class uses singleton pattern and you need to create
an instance of it with getInstance() static method like the code bellow:

import Logger from "ngn/dist/services/logger/Logger"

const logger = Logger.getInstance();
logger.error("ERROR", {notFrom: "NGNJs"});

logging in client-side

although you cannot use Logger class to log messages in client-side, an
API endpoint will be created for you called log on NGNJs initialization.
You can use that API to use logging feature client-side:

import axios from "axios";

axios.post("/api/log", {
    level: "ERROR",
    message: "ERROR",
    context: {notFrom: "NGNJs"}
})

Configuration

All NGNJs config files are located in config/ directory. This directory
is created by NGNJs initialization:

  1. rateLimit: This file is configuration for withThrottle middleware to
    throttle your users’ api requests. This middleware is enabled globally by
    default, but you can comment it if you want. You will learn how to set
    a middleware globally later in this section.

  2. trustedProxies: An array of ip addresses to be considered trusted. If
    request is sent to your application from these IPs, req.ip will be
    X-Forwarded-For or X-Real-Ip header in case of existence. Otherwise,
    req.ip will be remote address

  3. middlewares: You can manage global middlewares in this file. This file
    returns an object which maps endpoints to a group of middlewares.
    custom middlewares could be created using milkshake make middleware
    command.

Using global custom middlewares

Imagine you created a middleware called withUser which you want to apply
it to all api endpoint starts with /users. You can change middlewares
config file like bellow:

import withUser from "../src/middlewares/withUser"

const middlewares = {
    "/": [
        //All global middlewares
    ],
    "/users": [withUser]
}

export default middlewares;

Caching

NGNJs supports two driver for handling cache: File and Redis.
You need to have redis-server if you want to use Redis cache driver.

you can use cache ability by importing one of RedisCache or FileCache:

import RedisCache from "ngn/dist/services/cache/RedisCache";
import FileCache from "ngn/dist/services/cache/FileCache";

Methods supported by cache drivers:

  1. async get(key, defaultValue): returns value of key in cache storage if
    key is existed and default value otherwise.
  2. async set(key, value, ttl?): Sets a key in cache storage with the
    provided value for a specific time which is set with TTL
    (TTL is in seconds). If ttl is not provided, key will be kept forever
  3. async delete(key): Deletes a key from cache storage
  4. async clear(): Deletes all keys.
  5. async getMultiple(keys): Gets an array of keys and returns an object
    of key value pair.
  6. async setMultiple(object): Gets an object of key values and sets them
    in cache storage.
  7. async deleteMultiple(keys): Gets an array of keys and deletes them.
  8. async getTTL(key): Returns remaining time to live of a key.
  9. async has(key): Returns true if key is existed and false otherwise.

Static content

You may want to show an image you’ve uploaded before. A solution is to use
third party services like AS3. You can also access all of your files
located in storage/public/ with /api/storage endpoint. for example,
consider a file located in storage/public/images/1.jpg. You can view it
by calling /api/storage/images/1.jpg in browser.

CAUTION: Using storage endpoint is not optimized, and it’s highly
recommended to you to use aliases to point storage/public to /storage
with your webserver config.

creating file URL and path

You can use Path facade to generate URL and/or path of a file located in
storage directory. Path facade has some methods which is described
bellow by an example. (In this example, project is in
/var/www/http/ngn directory):

import Path from "ngn/dist/facades/Path";

const imagePath = '/images/1.jpg';

Path.makeStorageUrl(imagePath);
    //returns http://localhost:3000/api/storage/images/1.jpg
Path.makePublicStoragePath(imagePath);
    //returns /var/www/http/ngn/storage/public/images/1.jpg
Path.makeStoragePath(imagePath);
   //returns /var/www/http/ngn/storage/images/1.jpg
Path.makeUploadsPath(imagePath);
   //returns /var/www/http/ngn/storage/public/uploads/images/1.jpg

Note: You have to set STORAGE_BASE_URL env variable to use
makeStorageUrl method.