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

0.0.15 • Public • Published

proute

[WIP]

Typed file based express router

Currently only support typescript and vite-node

Overview

  • File based routing
    • nested folder structure
    • flat structure
  • Typed endpoint (path params, query params, body, responses)
  • middlewares
  • Input validation (path params, query params, body)
  • Autogenerated openapi documentation
  • Resources management

Getting started

Installation

yarn add -D proute

Setup

// vite.config.ts
import { defineConfig } from 'vitest/config';
import { prouteVitePlugin } from 'proute/plugins/vite';

export default defineConfig({
  plugins: [
    prouteVitePlugin({
      inputPath: './src/router', // folder where you are residing your endpoints files
      resourcesPath: './src/router/resources.ts', // file that exports resources schemas
      outputRouter: './src/router/proute.generated.router.ts', // file where the router will be generated
      outputRoutes: './src/router/proute.generated.routes.ts', // file where routes definitions will be generated
    }),
  ],
});

When vite-node server is started, the plugin will automatically generate a file at $outputPath which will export a router

Which can then be used with express app

// src/index.ts
import express from 'express';
import bodyParser from 'body-parser';
import { router } from './src/router/proute.generated.router';
const app = express();

// NOTE: proute will not parse body by itself and expect
// you to use a middleware like 'body-parser' to handle that part
app.use(bodyParser.json(), router);

app.listen(3000);

Features

File based routing

src/router
| # using a folder based structure
|-- products
|   |-- get.ts # GET /products
|   |-- post.ts # POST /products
|   |-- delete.ts # DELETE /products
|   |-- $id
|       |-- get.ts # GET /products/:id
|
|-- stores
    |-- get.ts # GET /stores
    |-- post.ts # POST /stores
    |-- delete.ts # DELETE /stores
    | # you can also use/combine with a flat style structure using `.` as a separator
    |-- $id.get.ts # GET /stores/:id
    |-- $id.patch.ts # PATCH /stores/:id
    |-- $id.products.get.ts # GET /stores/:id/products

Endpoint handler

Each endpoint handler file should export as default an object containing a conf field that define the endpoint specs and a handler field that will handle the endpoint.

Proute integrate with valibot for validation and typing

// src/router/products/get.ts
import { endpointConf, EndpointHandler } from 'proute';
import {
  array,
  object,
  nullish,
  number,
  string,
  union,
  literal,
} from 'valibot';
import { ROUTES } from '../base-conf.ts'; // 'base-conf' file is an auto generated file that expose the ROUTES object

const conf = endpointConf(ROUTE.get['/products'], {
  // schema of the expected querysearch params
  query: object({
    search: nullish(string()),
    limit: nullish(number()),
    offset: nullish(number()),
  }),

  // schema of possibles responses
  responses: {
    200: object({
      products: array(
        object({
          id: string(),
          name: string(),
        }),
      ),
    }),
    500: null, // null for empty response
  },
});

const handler: EndpointHandler<typeof conf> = async ({
  req,
  res,
  query: { search, limit, offset },
}) => {
  try {
    const products = await fetchProducts({ search, limit, offset });

    return {
      status: 200,
      data: {
        products,
      },
    };
  } catch {
    return {
      status: 500,
    };
  }
};

export default { conf, handler };
// src/router/products/$id/get.ts
import { endpointConf, EndpointHandler } from 'proute';
import { ROUTES } from '../../base-conf.ts'; // 'base-conf' file is an auto generated file that expose the ROUTES object

// Route params are automatically deduced from provided route, no schema is required 
const conf = endpointConf(ROUTES.get['/products/:id'], {
  // ...
});

const handler: EndpointHandler<typeof conf> = ({
  // route params will be available here
  params: { id },
}) => {
  // ...
};

Middlewares

The vite plugin will automatically generate a proute.utils.ts file. It will contain a helper named createMiddleware that allow you to create middlewares

Add extra param to endpoint handler

You can provide a function to createMiddleware that return param to be forwarded to any endpoint handler that use this middleware

export const addLogger = createMiddleware(({ req, res }) => {
  return {
    logger: createLogger(),
  };
});
// src/router/books.get.ts

const conf = expressConf(...).middleware(addLogger);

// logger is now accessible from handler first argument
const handler: EndpointHandler<typeof conf> = ({ logger }) => {

}

export default { conf, handler };

Authentication middleware

export const apiKeyAuthenticated = createMiddleware(
  {
    // A middleware can extends possible responses
    responses: {
      401: picklist(['UNAUTHENTICATED', 'INVALID_SCHEME']),
    },
  },
  async ({ req, res }) => {
    const auth = req.headers.authorization;

    if (!auth) {
      return {
        status: 401,
        data: 'UNAUTHENTICATED',
      };
    }

    if (!auth.startsWith('Bearer ')) {
      return {
        status: 401,
        data: 'INVALID_SCHEME',
      };
    }
    const apiKey = auth.slice(7);

    // your implementation
    if (!(await isValidApiKey(apiKey))) {
      return {
        status: 403,
        data: '',
      };
    }
    const roles = await getApiKeyRoles(apiKey);

    return {
      // you can then forward roles as extra param for endpoint handler/next middleware
      extraParam: {
        roles,
      },
    };
  },
);

File auto initializing

When the vite server is running, any empty endpoint file will be automatically initialized with a basic endpoint boilerplate

Docs

Openapi documentation will be generated automatically based on valibots schemas

Enable docs

// vite.config.ts
import { defineConfig } from 'vitest/config';
import { prouteVitePlugin } from 'proute/plugins/vite';

export default defineConfig({
  plugins: [
    prouteVitePlugin({
      // ...
      docs: {
        uiEndpoint: '/docs', // openapi ui using rapidocs
        jsonEndpoint: '/docs/openapi.json', // default: $uiEndpoint/openapi.json`
      },
    }),
  ],
});

Endpoint description

You can provide several metadata to endpointConf that will be included to the generated openapi json

// src/router/books.get.ts
const conf = endpointConf(ROUTE.get['/books'] {
  summary: 'Get books',
  description: 'Get all books',
  tags: ['books'],
  // ...
});

Security Schemes

You can create a config.ts file inside your router folder that will be automatically loaded. This config file should export a proute config object.

// src/router/config.ts
import { prouteConfig } from 'proute';

export default prouteConfig({
  securitySchemes: {
    ApiKey: {
      type: 'http',
      scheme: 'bearer',
    },
  },
});

You can then specify that a route require this authentication scheme using the security field of the route conf

// src/router/books.get.ts

const conf = expressConf(ROUTE.get['/books'], {
  security: ['ApiKey'],
});

// ...

A middleware can also specify that it will apply the authentication scheme. The security field will then be forwarded to any route that use this middleware

//...
export const authenticated = createMiddleware(
  {
    security: ['ApiKey'],
    responses: {
      401: null,
      403: null,
    },
  },
  async function authenticatedMdw({ req }) {
    // authentication implementation ...
  },
);

Valibot action

Valibot action can be used to add extra validation / metadata

// src/router/books.get.ts
import {
  pipe,
  decimal,
  transform,
  string,
  object,
  description, // `description` action can be used to add description to the generated openapi json
} from 'valibot';
import { example } from 'proute'; // Proute provide an `example` action that allow you to add examples metadata

const conf = endpointConf(ROUTE.get['/books'], {
  summary: 'Get books',
  description: 'Get all books',
  tags: ['books'],
  // ...
  query: object({
    limit: pipe(
      decimal(),
      transform((v) => Number(v)),
      minValue(1),
      maxValue(10),
      description('Limit number of books in response'),
    ),
  }),
  responses: {
    200: object({
      books: pipe(
        object({
          id: pipe(string(), description('Book id')),
        }),
        description('List of books'),
        example({
          id: 'book_1',
        }),
        example({
          id: 'book_2',
        }),
      ),
    }),
  },
});

Readme

Keywords

none

Package Sidebar

Install

npm i proute

Weekly Downloads

6

Version

0.0.15

License

none

Unpacked Size

329 kB

Total Files

14

Last publish

Collaborators

  • shantry