Overview
WAPI uses a middleware pattern inspired by Express.js and Koa.js, allowing you to compose reusable message handlers that execute in sequence. Middleware functions can process messages, modify the context, perform authentication checks, logging, and more.
Composable Chain multiple handlers together for modular code
Order Matters Middlewares execute in registration order
Flow Control Use next() to control execution flow
Global & Command Apply middlewares globally or per-command
Middleware Function Signature
From src/types/bot.ts lines 28-29:
export type NextFn = () => Promise < void >;
export type MiddlewareFn = ( ctx : Context , next : NextFn ) => Promise < void >;
Every middleware function receives:
ctx : The Context object containing message data and reply methods
next : Async function to pass control to the next middleware
Basic Usage
Global Middleware
Global middlewares run for every incoming message :
import { Bot , LocalAuth } from 'wapi' ;
import { randomUUID } from 'crypto' ;
const bot = new Bot ( randomUUID (), new LocalAuth ( randomUUID (), './sessions' ), {
jid: '' , pn: '' , name: ''
});
// This runs for ALL messages
bot . use ( async ( ctx , next ) => {
console . log ( `Message from: ${ ctx . from . name } ` );
console . log ( `Text: ${ ctx . text } ` );
await next (); // Pass to next middleware
});
await bot . login ( 'qr' );
Registering Multiple Middlewares
From src/core/bot.ts lines 38-40:
public use ( ... middlewares : MiddlewareFn []): void {
this . middlewares . push ( ... middlewares );
}
You can register multiple middlewares at once:
const logger = async ( ctx , next ) => {
console . log ( '[LOG]' , ctx . text );
await next ();
};
const timer = async ( ctx , next ) => {
const start = Date . now ();
await next ();
console . log ( `Processed in ${ Date . now () - start } ms` );
};
const authenticator = async ( ctx , next ) => {
if ( isAuthorized ( ctx . from . jid )) {
await next ();
} else {
await ctx . reply ( 'Unauthorized!' );
}
};
// Register all at once
bot . use ( logger , timer , authenticator );
The next() Function
The next() function is crucial for middleware flow control:
Calling next()
// ✅ CORRECT: Calls next middleware
bot . use ( async ( ctx , next ) => {
console . log ( 'Before' );
await next (); // Passes control to next middleware
console . log ( 'After' );
});
Not Calling next()
// ✅ CORRECT: Stops execution chain
bot . use ( async ( ctx , next ) => {
if ( ctx . text === 'stop' ) {
await ctx . reply ( 'Execution stopped' );
// NOT calling next() - chain stops here
return ;
}
await next (); // Only called if text !== 'stop'
});
Multiple next() Calls
// ❌ ERROR: Will throw "next() called multiple times."
bot . use ( async ( ctx , next ) => {
await next ();
await next (); // ERROR!
});
From src/core/bot.ts lines 185-187:
if ( i <= index ) {
throw new Error ( "next() called multiple times." );
}
Each middleware can only call next() once. Multiple calls will throw an error to prevent infinite loops.
Execution Flow
Middlewares execute in a stack-like order:
bot . use ( async ( ctx , next ) => {
console . log ( '1: Before' );
await next ();
console . log ( '1: After' );
});
bot . use ( async ( ctx , next ) => {
console . log ( '2: Before' );
await next ();
console . log ( '2: After' );
});
bot . use ( async ( ctx , next ) => {
console . log ( '3: Handler' );
await ctx . reply ( 'Done!' );
// No next() call - end of chain
});
// Output:
// 1: Before
// 2: Before
// 3: Handler
// 2: After
// 1: After
Flow Diagram
Command-Specific Middleware
From src/core/bot.ts lines 41-46:
public command ( name : string , ... middlewares : MiddlewareFn []): void {
if ( ! name ) {
throw new Error ( "The command name must be at least 1 character long." );
}
this . commands . set ( name , middlewares );
}
Command middlewares only run when a specific command is detected:
bot . command ( 'hello' , async ( ctx ) => {
await ctx . reply ( `Hello, ${ ctx . from . name } !` );
});
bot . command ( 'ping' , async ( ctx ) => {
await ctx . reply ( 'Pong!' );
});
// Multiple middlewares for one command
bot . command ( 'admin' ,
async ( ctx , next ) => {
// Authorization check
if ( ctx . from . jid === 'admin@lid' ) {
await next ();
} else {
await ctx . reply ( 'Admin only!' );
}
},
async ( ctx ) => {
// Admin handler
await ctx . reply ( 'Admin command executed' );
}
);
Middleware Composition
From src/core/bot.ts lines 178-198, here’s how middlewares are composed:
// Build middleware chain: global + command-specific
const middlewares = [
... this . middlewares , // Global middlewares first
... ( this . commands . get ( ctx . commandName ) ?? []), // Then command middlewares
];
if ( middlewares . length ) {
let index = - 1 ;
const runner = async ( i : number ) : Promise < void > => {
if ( i <= index ) {
throw new Error ( "next() called multiple times." );
}
index = i ;
const fn = middlewares [ i ];
if ( ! fn ) {
return ; // End of middleware chain
}
await fn ( ctx , async () => {
await runner ( i + 1 ); // Recursively call next middleware
});
};
await runner ( 0 ); // Start execution
}
Execution Order
Global middlewares (in registration order)
Command middlewares (if command matches)
// Global middleware 1
bot . use ( async ( ctx , next ) => {
console . log ( 'Global 1' );
await next ();
});
// Global middleware 2
bot . use ( async ( ctx , next ) => {
console . log ( 'Global 2' );
await next ();
});
// Command middleware
bot . command ( 'test' , async ( ctx ) => {
console . log ( 'Command handler' );
});
// When user sends "!test":
// Output:
// Global 1
// Global 2
// Command handler
Real-World Examples
const logger = async ( ctx , next ) => {
const timestamp = new Date (). toISOString ();
console . log ( `[ ${ timestamp } ] ${ ctx . from . name } : ${ ctx . text } ` );
await next ();
};
bot . use ( logger );
const rateLimit = new Map < string , number >();
const rateLimiter = async ( ctx , next ) => {
const now = Date . now ();
const lastMessage = rateLimit . get ( ctx . from . jid ) || 0 ;
if ( now - lastMessage < 1000 ) {
await ctx . reply ( 'Please wait before sending another message.' );
return ; // Stop execution
}
rateLimit . set ( ctx . from . jid , now );
await next ();
};
bot . use ( rateLimiter );
const groupOnly = async ( ctx , next ) => {
if ( ctx . chat . type === 'group' ) {
await next ();
} else {
await ctx . reply ( 'This command only works in groups.' );
}
};
bot . command ( 'groupinfo' , groupOnly , async ( ctx ) => {
await ctx . reply ( `Group: ${ ctx . chat . name } ` );
});
const errorHandler = async ( ctx , next ) => {
try {
await next ();
} catch ( error ) {
console . error ( 'Error processing message:' , error );
await ctx . reply ( 'An error occurred. Please try again.' );
}
};
bot . use ( errorHandler );
const analytics = async ( ctx , next ) => {
if ( ctx . commandName ) {
console . log ( `Command used: ${ ctx . commandName } ` );
console . log ( `Args: ${ ctx . args . join ( ', ' ) } ` );
console . log ( `User: ${ ctx . from . name } ` );
// Send to analytics service
}
await next ();
};
bot . use ( analytics );
const preprocessor = async ( ctx , next ) => {
// Trim whitespace
ctx . text = ctx . text . trim ();
// Convert to lowercase for case-insensitive commands
// (commandName is already lowercase, but this helps for text matching)
const originalText = ctx . text ;
ctx . text = ctx . text . toLowerCase ();
await next ();
// Restore original text after processing
ctx . text = originalText ;
};
bot . use ( preprocessor );
Advanced Patterns
Conditional Middleware
const conditionalMiddleware = ( condition : ( ctx : Context ) => boolean , ... middlewares : MiddlewareFn []) => {
return async ( ctx : Context , next : NextFn ) => {
if ( condition ( ctx )) {
// Execute middlewares only if condition is true
let index = - 1 ;
const runner = async ( i : number ) : Promise < void > => {
if ( i <= index ) throw new Error ( "next() called multiple times." );
index = i ;
const fn = middlewares [ i ];
if ( ! fn ) {
await next (); // Continue to next global middleware
return ;
}
await fn ( ctx , async () => await runner ( i + 1 ));
};
await runner ( 0 );
} else {
await next ();
}
};
};
// Usage
bot . use (
conditionalMiddleware (
( ctx ) => ctx . chat . type === 'group' ,
async ( ctx , next ) => {
console . log ( 'This only runs in groups' );
await next ();
}
)
);
Middleware Factories
function authorize ( allowedJids : string []) {
return async ( ctx : Context , next : NextFn ) => {
if ( allowedJids . includes ( ctx . from . jid )) {
await next ();
} else {
await ctx . reply ( 'You are not authorized to use this command.' );
}
};
}
// Usage
const adminOnly = authorize ([ 'admin1@lid' , 'admin2@lid' ]);
bot . command ( 'restart' , adminOnly , async ( ctx ) => {
await ctx . reply ( 'Restarting...' );
process . exit ( 0 );
});
Async Data Loading
const loadUserData = async ( ctx , next ) => {
// Attach user data to context
ctx [ 'userData' ] = await database . getUserData ( ctx . from . jid );
await next ();
};
bot . use ( loadUserData );
bot . command ( 'profile' , async ( ctx ) => {
const userData = ctx [ 'userData' ];
await ctx . reply ( `Name: ${ userData . name } \n Level: ${ userData . level } ` );
});
Best Practices
Always await next()
// ✅ CORRECT
await next ();
// ❌ WRONG - Missing await
next ();
Place error handlers first
bot . use ( errorHandler ); // First
bot . use ( logger ); // Then other middlewares
bot . use ( rateLimiter );
Keep middlewares focused
Each middleware should have a single responsibility (logging, auth, etc.).
Use early returns
bot . use ( async ( ctx , next ) => {
if ( ! isValid ( ctx )) {
await ctx . reply ( 'Invalid message' );
return ; // Stop here
}
await next ();
});
Don't modify ctx.message
The original ctx.message object should not be modified. Use separate properties instead.
Common Pitfalls
Forgetting to call next() If you forget to call next(), subsequent middlewares won’t execute: // ❌ BAD - next() never called
bot . use ( async ( ctx , next ) => {
console . log ( 'This runs' );
// Forgot await next();
});
bot . use ( async ( ctx , next ) => {
console . log ( 'This NEVER runs' );
});
Calling next() multiple times // ❌ BAD - throws error
bot . use ( async ( ctx , next ) => {
await next ();
await next (); // Error!
});
Not handling errors in async code // ❌ BAD - uncaught promise rejection
bot . use ( async ( ctx , next ) => {
const data = await fetchData (); // May throw
await next ();
});
// ✅ GOOD - wrapped in try-catch
bot . use ( async ( ctx , next ) => {
try {
const data = await fetchData ();
await next ();
} catch ( error ) {
console . error ( error );
await ctx . reply ( 'Failed to fetch data' );
}
});
Debugging Middleware
const debugMiddleware = async ( ctx , next ) => {
console . log ( '--- Middleware Debug ---' );
console . log ( 'From:' , ctx . from . name );
console . log ( 'Text:' , ctx . text );
console . log ( 'Command:' , ctx . commandName );
console . log ( 'Args:' , ctx . args );
console . log ( 'Chat Type:' , ctx . chat . type );
console . log ( '------------------------' );
await next ();
};
bot . use ( debugMiddleware );
Next Steps
Commands Learn about the command system
Context Master the Context API