npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

alado

v1.7.3

Published

Extremely fast, lightweight, robust framework for HTTP applications written in Node.js

Downloads

234

Readme

Alado is an extremely fast, simple, robust, and lightweight (no-dependency) for creating server applications written in Node.js and running over the HTTP protocol.

Its main difference from most similar well-known frameworks is that with a minimum of external dependencies it provides out-of-the-box such fully-functioning features as routing, CORS, automatic API documentation (Open API 3.0), parsing query and path parameters, and the request body, file uploading, request authentication, etc.

The basis of use is a combination of interface simplicity and separation of the “routine” part of writing API from business logic implementation - due to a declarative approach to describing all request parameters in the form of static reusable structures.

There is an example of the API creating with Alado:

https://github.com/databikers/alado-api-example

  1. Installation
npm i -s alado
  1. Instancing
import { AladoServer } from 'alado';

const app: AladoServer = new AladoServer({
  port: 3000,
  cors: {
    enable: true,
    allowedOrigin: '*',
    allowedHeaders: ['Authorization'],
    exposeHeaders: ['x-total-count'],
  },
  openApiDoc: {
    enable: true,
    route: '/open-api',
    info: {
      title: 'My API title',
      description: 'My API description',
      version: '1.0.0',
    },
  },
  verbose: true
});

In case you want to run an HTTPs server (even though normally a Node.js app runs behind nginx/ingress etc.) You can do it using the ssl parameter (look at SecureContextOptions in the "tls" module - Node.js TLS module:

import { readFileSync } from 'fs';
import { AladoServer } from 'alado';

const app = new AladoServer({
  port: 3000,
  ssl: {
    cert: readFileSync('/path/to/cert'),
    key: readFileSync('/path/to/key'),
  },
  // https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener
  serverOptions: {
    keepAliveTimeout: 5000,
  },
  cors: {
    enable: true,
    allowedOrigin: '*',
    allowedHeaders: ['Authorization'],
    exposeHeaders: ['x-total-count'],
    allowedCredentials: true,
    maxAge: 3600
  },
  headers: {
    'a': 'b' //any additional header
  },
  openApiDoc: {
    enable: true,
    route: '/',
    info: {
      title: 'My API title',
      description: 'My API description',
      version: '1.0.0',
    },
  },
  verbose: true
});
  1. Defining routes

Here is how you define routes in Alado:

app.get('/user/:id', context, (request: Request): Response<UserDto> => {
  const user = {
    name: 'John Doe',
    id: request.path.id,
  };
  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json',
    },
    body: user,
  };
});

app.start(() => console.log(`[API]: application has been launched successfully`));

The application provides "get", "post", "delete", "patch", "put" and "head" methods for defining routes. These methods accept three required arguments:

  • route or path: A string that may contain variables to specify the route.

  • context: An aggregating object that fully describes the request.

  • handler: A function that handles the request.

    3.1 Route or path.

Path variables defining works in the same way as it works in other frameworks: You just define a part of your route (means part separated by slashes) as ":id" and receive the passed value as request.path.id).

3.2 Context

Here's the contract of the request context:

interface Context<T> {
  title: string;
  auth?: RequestAuthentication;
  options: ContextOptions;
  request: ContextRequest;
  response: Response<T>;
}

Unlike context.title, which is simply a unique string, the other properties may not be immediately clear. However, once explained, they become quite obvious and extremely logical.

Note that all parts of the context and its nested properties can be reused

3.2.1 Context.auth

import { RequestAuthentication } from 'alado';

const bearerAuth: RequestAuthentication = {
  inputProperty: 'headers.authorization',
  outputProperty: 'auth.user',
  handler: async (bearer: string) => {
    // validate header, return user
    return {
      name: 'John Doe',
      id: 1,
    };
  },
  required: true, // false You want "soft" checking
  error: {
    statusCode: 401,
    message: 'Unauthorized',
  },
};

This means that the application takes an "Authorization" header (or "api_token" query parameter if you provide "query.api_token" as an inputProperty, "body.someThing" and "path.token" work also) use the provided handler to get the user and set the received user to request.auth.user, (and the mentioned "request" here is the request argument in your route handler) Or just returns 401 Unauthorized. This means that the application can authenticate using an "Authorization" header, or alternatively, using an "api_token" query parameter (if specified as "query.api_token" in the inputProperty). It can also authenticate using values like "body.someThing" or "path.token". In the example the provided handler retrieves the user and assigns it to request.auth.user (where request is the argument in your route handler). If authentication fails, it returns a 401 Unauthorized response.

This also works:

import { RequestAuthentication } from 'alado';

const bearerAuth: RequestAuthentication = {
  inputProperty: 'header.x-api-token',
  outputProperty: 'auth',
  handler: async (xApiToken: string) => {
    // You will receive user and company as nested properties in request auth
    return {
      user: {
        name: 'John Doe',
        id: 1,
      },
      company: {
        name: 'Databikers Limited',
        id: 2,
      },
    };
  },
  //optional property if You want to specify execution context for the auth handler
  handlerContext: {},
  required: true, // false You want "soft" checking
  error: {
    statusCode: 401,
    message: 'Unauthorized',
  },
};

3.2.2 Context.options

interface ContextOptions {
  allowUnknownFields?: boolean;
  openApiDoc?: {
    description?: string;
    operationId: string;
    tags?: string[];
  };
}

Setting "allowUnknownFields" to false protects your application from receiving unexpected properties in the request body, query, and files. It does not affect request.path, as path variables are defined at the routing level, nor does it affect request headers. Conversely, you can receive { name: 'John Doe', role: 'Admin' } when only expecting name.

The "openApiDoc" property is part of the Open API route definition and is optional.

3.2.3 Context.request

Context.request is aggregating object that describes all HTTP request properties You expect

interface ContextRequest {
  headers?: Record<string, PropertyDefinition>;
  query?: Record<string, PropertyDefinition>;
  path?: Record<string, PropertyDefinition>;
  body?: Record<string, PropertyDefinition>;
  files?: Record<string, FilePropertyDefinition>;
  auth?: Record<string, any>;
}

There's an example for request with path property only:

const request: ContextRequest = {
  path: {
    id: {
      openApiDoc: {
        schema: {
          type: 'string',
        },
        example: '123456',
        description: 'User id',
      },
      validation: {
        required: true,
        handler(value) {
          return /^\d+$/.test(value);
        },
        transform(value) {
          return parseInt(value, 10);
        },
        error: {
          statusCode: 400,
          message: '400.1.0',
        },
      },
    },
  },
};

Validation.handler returns boolean (any => boolean | any => Promise) if received value fits to provided requirements, and in falsy case it throws an error and app returns 400 Bad Request with a json body contains specified message Handler can be async and can use complex logic under the hood; it allows to use Avoid using an arrow functions as handlers - in this case You can lose execution context, The execution context of validation.handler (and validation transform) is the request (aggregating object - the argument used at the route handler)

const request: ContextRequest = {
  body: {
    role: {
      openApiDoc: {
        schema: {
          type: 'string',
        },
        example: 'admin',
        description: 'User role',
      },
      validation: {
        required: true,
        async handler(value) {
          if (this.auth.user.role === 'admin') {
            return true;
          }
          await userService.makeSomeChecking(this.query.scope);
          return value === 'customer';
        },
        error: {
          statusCode: 400,
          message: '400.1.0',
        },
      },
    },
  },
};

In the same way You can describe the request headers, query, body. Remember, if context.options.allowUnknownFields is set to false, all non-described request properties (or non-described nested properties in the described request properties) never be accessible.

const request: ContextRequest = {
  query: {
    sortBy: {
      openApiDoc: {
        schema: {
          type: 'string',
        },
        example: 'createdAt',
        description: 'Sort by field',
      },
      validation: {
        required: true,
        handler(value) {
          return [
            'createdAt',
            'updatedAt',
          ].includes(value);
        },
        error: {
          statusCode: 400,
          message: 'sortBy should be createdAt or updatedAt',
        },
      },
    },
    sortOrder: {
      openApiDoc: {
        schema: {
          type: 'string',
        },
        example: 'asc',
        description: 'Sort order',
      },
      validation: {
        required: true,
        handler(value) {
          return [
            'asc',
            'desc',
          ].includes(value);
        },
        error: {
          statusCode: 400,
          message: 'sortOrder should be asc or desc',
        },
      },
    },
  },
};

Now, if you send GET /user?sortBy=updateAt&sortOrder=desc&limit=10, the limit parameter will be ignored since it wasn't described:

app.post('/user', context, (request: Request) => {
  console.log(request.query);

  // {
  //   sortBy: 'updatedAt',
  //   sortOrder: 'desc'
  // }

  console.log(request.ip);
  
  // 127.0.0.1


  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json',
    },
    body: [],
  };
});

Regarding the request body even it described properly it will be available only for POST, PUT and PATCH HTTP requests. If You want dealing with uploaded files You just should to describe it in the request:

const request: ContextRequest = {
  files: {
    avatar: {
      mimetypes: ['image/png'],
      maxSize: 1048576,
      required: true,
      maxSizeError: {
        statusCode: 413,
        message: 'The avatar should not be larger than 1MB',
      },
      mimetypeError: {
        statusCode: 415,
        message: 'The avatar should be a PNG image',
      },
      requiredError: {
        statusCode: 400,
        message: 'The avatar file is required',
      },
    },
  },
};

and uploaded file will be acceptable in the request:

import { createWriteStream } from 'fs';

app.post('/user/:id/avatar', context, (request: Request) => {
  console.log(request.files);
  // {
  //   avatar: {
  //     stream: Readable,
  //     size: 65025,
  //     mimetype: 'image/png'
  //   }
  // }

  // You can play with request.files.avatar.stream as You want e.g. save it locally

  const writeStream = createWriteStream(`/path/to/user/avatars/${req.path.id}.png`);
  request.files.avatar.stream.pipe(writeStream);
  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json',
    },
    body: user,
  };
});

3.2.4 Context response

Context.response is always the same as the return type of the route handler

interface Response<T> {
  title?: string;
  description?: string;
  statusCode: number;
  headers?: Record<string, string>;
  body?: T;
}

3.3 Route handler

The route handler is the function that implements the following contract:

(request: Request) => Response<T> | Promise<Response<T>>;

Despite having a strict contract for the return type, the response body can be an object (for application/json content-type), text, or even a stream

interface UserDto {
  id: number;
  name: string;
}

const handlerWithJSONBody = (request: Request): Response<UserDto> => {
  const body: UserDto = {
    id: 1,
    name: 'John Doe',
  };
  return {
    statusCode: 200,
    headers: { 'Content-Type': 'application/json' },
    body,
  };
};

const handlerWithStringBody = (request: Request): Response<string> => {
  const body: string = 'Not Found';
  return {
    statusCode: 404,
    headers: { 'Content-Type': 'text/plain' },
    body,
  };
};

const handlerWithStreamBody = (request: Request): Response<ReadStream> => {
  const body = createReadStream('/path/to/image.png');
  return {
    statusCode: 200,
    headers: { 'Content-Type': 'image/png' },
    body,
  };
};
  1. Running
app.start(() => console.log('Your awesome application has been successfully launched'));

// And stop the application when needed
app.stop(() => console.log('Your awesome application has been successfully launched'));

Don't forget to check swagger at the defined above options.openApiDoc.route