Giter Site home page Giter Site logo

appgeist / restful-api Goto Github PK

View Code? Open in Web Editor NEW
1.0 2.0 0.0 368 KB

An opinionated, convention-over-configuration Express-based restful API server featuring yup validation

License: ISC License

JavaScript 100.00%
nodejs api validation rest rest-api api-server rest-server yup validation-library validation-engine

restful-api's Introduction

@appgeist/restful-api

NPM version License

AppGeist Restful API

An opinionated, convention-over-configuration Express-based restful API server featuring yup validation.

Usage

Initialization

You can use @appgeist/restful-api either as an Express middleware:

const express = require("express");
const api = require("@appgeist/restful-api");

const [host, port] = ["0.0.0.0", 3000];

express()
  // other middleware
  .use(api())
  // other middleware
  .listen(port, host, err => {
    if (err) throw err;
    // eslint-disable-next-line no-console
    console.log(`Server listening on http://${host}:${port}...`);
  });

...or directly:

const api = require("@appgeist/restful-api");

const [host, port] = ["0.0.0.0", 3000];

api().listen(port, host, err => {
  if (err) throw err;
  // eslint-disable-next-line no-console
  console.log(`Server listening on http://${host}:${port}...`);
});

When initializing @appgeist/restful-api you can optionally specify the folder containing the route handler definitions (defaults to ./routes):

const path = require("path");
const api = require("@appgeist/restful-api");

const [host, port] = ["0.0.0.0", 3000];

api(path.join(__dirname, "api-routes")).listen(port, host, err => {
  if (err) throw err;
  // eslint-disable-next-line no-console
  console.log(`Server listening on http://${host}:${port}...`);
});

Defining route handlers

Name your route handler definition modules get.js, post.js, put.js, patch.js or delete.js and organize them in a meaningful folder structure that will get translated to rest API routes.

For instance:

routes/departments/get.js => GET /departments
routes/departments/post.js => POST /departments
routes/departments/[id]/patch.js => PATCH /departments/3
routes/departments/[id]/delete.js => DELETE /departments/3
routes/departments/[id]/employees/get.js => GET /departments/3/employees
routes/departments/[id]/employees/post.js => POST /departments/3/employees
routes/departments/[id]/employees/[id]/get.js => GET /departments/3/employees/165
routes/departments/[id]/employees/[id]/patch.js => PATCH /departments/3/employees/165
routes/departments/[id]/employees/[id]/delete.js => DELETE /departments/3/employees/165

Each module exports:

  • beforeRequest - an optional before request handler function;
  • onRequest - a mandatory request handler function;
  • paramsSchema, querySchema, bodySchema - optional schemas to validate the incoming request params, query and body. These can be simple objects (for brevity, in which case they will be converted to yup schemas automatically) or yup schemas (for more complex scenarios, i.e. when you need to specify a .noUnknown() modifier).

A function handler accepts an optional object parameter in the form of { params, query, body, req } and must return the data that will be sent back to the client, or a Promise resolving with the data.

For simple cases when you don't care about validation, you can export just the handler, like so: module.exports = () => {/* handle request here... */};.

Nested paths

For nested paths, ancestor-level parameter names are magically renamed with the help of pluralize.singular:

routes/departments/[id]/employees/[id]/get.js -> params: { departmentId, id } routes/departments/[id]/employees/[id]/projects/[id]/get.js -> params: { departmentId, projectId, id }

Default error handling

Validation errors

When request data validation fails, the client will receive a response with HTTP status code 400/BAD_REQUEST and a JSON body describing the error provided by yup.validate(data, { abortEarly: false }):

{
  "message": "There were 2 validation errors",
  "errors": ["body.name is required", "body.description is required"]
}

Throwing errors

If a function handler throws an ApiError(httpStatusCode) (also exported from this package), the client will receive a response with the specified HTTP status code and a JSON body like { "message": "Why it failed" }.

The ApiError constructor accepts an HTTP status code number or an object structured like { status, message }.

Using the constructor without parameters will result in a respose with HTTP status code 500 and the following JSON body:

{
  "message": "Internal Server Error"
}

Custom error handling

You can override the default error handling mechanism by providing a custom error handling function like so:

const path = require("path");
const api = require("@appgeist/restful-api");

const [host, port] = ["0.0.0.0", 3000];

api(path.join(__dirname, "api-routes"), {
  errorHandler: ({ err, res }) => {
    res.status(500).send("Error");
    console.log(err.stack);
  }
}).listen(port, host, err => {
  if (err) throw err;
  // eslint-disable-next-line no-console
  console.log(`Server listening on http://${host}:${port}...`);
});

Example

routes/departments/get.js:

const Department = require("models/Department");

// simple handler, without validation
module.exports = () => Department.listAll();
// ...or exports.onRequest = () => Department.listAll();

routes/departments/[id]/patch.js:

const { object, number } = require('yup');
const { FORBIDDEN } = require('http-status-codes');
const { ApiError } = require('@appgeist/rest-api');
const Department = require('models/Department');
const ACL = require('utils/acl');
const Logger = require('utils/logger');

// schema can be a simple object
exports.paramsSchema = {
  id: number().positive().integer().required()
};

// schema can be a yup object
exports.bodySchema = object({
  name: string().min(2).max(20).required()
  description: string().min(2).max(1000).required()
}).noUnknown();

exports.onRequest = async ({ params: { id }, body, req }) => {
  // throwing an ApiError(403) will result in a 403 status code being sent to the client
  if (!(ACL.isManager(req.userId))) throw new ApiError(FORBIDDEN);
  const department = await Department.updateById(id, { data: body });
  await Logger.log(`Department ${id} updated by user ${req.userId}`);
  return department;
}

routes/departments/[id]/employees/[id]/get.js:

const { number } = require('yup');
const Department = require('models/Department');
const Employee = require('models/Employee');

exports.paramsSchema = {
  departmentId: number().positive().integer().required()
  id: number().positive().integer().required()
};

exports.beforeRequest = ({ url }) => {
  console.log(`Preparing to respond to ${url}`);
};

exports.onRequest = async ({ params: { departmentId, id } }) => {
  const employee = await Employee.getById(id);
  employee.department = await Department.getById(departmentId);
  return employee;
}

License

The ISC License.

restful-api's People

Contributors

dependabot[bot] avatar icflorescu avatar

Stargazers

 avatar

Watchers

 avatar  avatar

restful-api's Issues

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.