[WIP]
Typed file based express router
Currently only support typescript and vite-node
- 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
yarn add -D proute
// 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);
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
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 },
}) => {
// ...
};
The vite plugin will automatically generate a proute.utils.ts
file. It will contain a helper named createMiddleware
that allow you to create middlewares
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 };
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,
},
};
},
);
When the vite server is running, any empty endpoint file will be automatically initialized with a basic endpoint boilerplate
Openapi documentation will be generated automatically based on valibots schemas
// 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`
},
}),
],
});
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'],
// ...
});
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 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',
}),
),
}),
},
});