Creating an Application
This section demonstrates a simple program using LiteMW, covers the features of our middlewares with usage examples, router capabilities, and working with context state.
First Program
Let's look at the minimal code to start an application.
import { createRouter } from '@litemw/router';
import Koa from 'koa';
const router = createRouter('/api');
router.get('/endpoint', (ctx) => {
console.log(`Get request handled`);
ctx.body = 'response'
})
const app = new Koa();
app.use(router.routes());
app.listen(3000)
Here, the createRouter function from the @litemw/router package is used to create a LiteMW router. It accepts optional arguments: a prefix that will be applied to each child handler and options identical to those of the Koa router. Schematically, its type can be represented as:
function createRouter<...>(
prefix?: string,
opts?: RouterOptions
): Router<...>;
In reality, its type is more complex because the prefix and methods parameterize the router type for compile-time checks.
The RouterOptions interface is as follows:
interface IRouterOptions {
prefix?: string | undefined; // Prefix
methods?: string[] | undefined; // Methods
sensitive?: boolean | undefined; // Case sensitivity
strict?: boolean | undefined; // Strict checking (considering the number of slashes)
}
The router also implements the IBaseRouter interface from the @koa/router library and contains the base router in the koaRouter field:
const router = createRouter()... // satisfies IBaseRouter
type T = typeof router.koaRouter // KoaRouter
Middleware Principles
In LiteMW, middlewares are used both for building handlers and routers via the use(...) method. Each middleware populates the context state during request processing. Schematically, the method type can be represented as:
function use<NewState extends State, Return>(
mw: (ctx {..., state: NewState}, next: NextFunction) => Return
): RouterOrHandler<State & Return>;
Where:
State
is the current state type of the router or handler,NewState
is the state type expected by the middleware,Return
is the middleware's return type.
After execution, the middleware's returned value is added to the context state (overwriting existing properties). The use method returns the same router or handler but parameterized with a new type.
Examples
Let's look at a few middleware usage examples. Suppose we want to parse the request body in the parseBody middleware and validate it in validateBody:
const parseBody: Middleware<any, { someBody: string }> = async (ctx) => {
const someBody = '';
await new Promise(resolve => {
ctx.req.on('data', (chunk) => {/* collect body to string */});
ctx.req.on('end', resolve)
})
return { someBody };
};
const validateBody: Middleware<{ someBody: string }> = (ctx) => {
// do validation
};
parseBody extracts data from the request (very schematically) and returns a string someBody in the context state. validateBody performs validation on the someBody field and returns nothing. As we can see, parseBody expects any incoming state type, while validateBody assumes the context state must contain the someBody field.
Let's try using them:
const router = createRouter('/api');
router.get('/endpoint')
.use(parseBody) // Ok
.use(validateBody) // Ok
.use((ctx) => {
console.log(ctx.state.someBody); // string
})
During request processing, the body will be retrieved, validated, and printed. But what if we swap their order?
const router = createRouter('/api');
router.get('/endpoint')
.use(validateBody) // TS2345: Argument of type ... is not assignable to ...
.use(parseBody)
.use((ctx) => {
console.log(ctx.state.someBody); // string
})
We'll see a compilation error because the state type lacks the fields expected by the middleware. A similar error occurs if parseBody is removed entirely.
Nested Routers
The router's use method can connect another router with an optional prefix.
const v1Router = createRouter('/v1')
... // some definitions
const latestRouter = createRouter()
... // some definitions
const apiRouter = createRouter('/api')
apiRouter.use(v1Router)
apiRouter.use('/latest', latestRouter)
Note that the router's middlewares will be applied in the request processing chain of nested routers but won't be reflected in their type. Therefore, it's recommended to connect routers only to a main router.
Default State
Even without any middlewares, the context state will contain:
- router: Available in both router and handler middlewares.
- handler: Available only in handler middlewares.
These provide access to prefix, route, and all router/handler properties.
const router = createRouter('/api')
.use(ctx => {
ctx.router // <Router>
});
router.get('/endpoint', (ctx) => {
ctx.router // <Router>
ctx.handler // <RouteHandler>
})
State Augmentation
For global middlewares that provide context content available in all subsequent middlewares, you can augment the state type as follows:
declare module '@litemw/router' {
interface DefaultState {
someString: string;
someNumber: number;
}
}
The specified fields will be available in all handlers.
const router = createRouter('/api')
.use(ctx => {
ctx.someString // string
ctx.someNumber // string
});
router.get('/endpoint', (ctx) => {
ctx.someString // string
ctx.someNumber // string
})
Handling chain interruption
Sometimes you may need to interrupt the request processing chain. This can be done by calling the cancel() method on the next object.
Example:
const router = createRouter('/api')
.use(async (ctx, next) => {
console.log('First middleware')
await next()
console.log('After first middleware')
})
.use((ctx, next) => {
console.log("Interrupted")
ctx.body = "Interrupted"
next.cancel()
})
.use(ctx => {
... // do some work
});
router.get('/endpoint', (ctx) => {
...
})
When accessing /api/endpoint, the string 'Interrupted' will be returned. After next.cancel(), subsequent middlewares won't execute, and control returns to previous middlewares where next was called.
Console output:
First midddleware
Interrupted
After first middleware
This mechanism is useful when you need to terminate request processing but still execute previous middlewares in the chain. An alternative is throwing an exception, which requires a try/catch block in one of the preceding middlewares.