Documentation Index
Fetch the complete documentation index at: https://mintlify.com/grammyjs/grammY/llms.txt
Use this file to discover all available pages before exploring further.
Creating your own plugins allows you to encapsulate reusable functionality and share it across projects or with the community. This guide shows you how to build custom middleware and plugins using grammY’s powerful Composer API.
Understanding Middleware
At its core, a plugin is just middleware. Every middleware function receives two parameters:
import { Context, NextFunction } from 'grammy'
function myMiddleware(ctx: Context, next: NextFunction) {
// ctx: The context object with update information
// next: Function to call downstream middleware
// Do something before downstream handlers
console.log('Before')
// Pass control to the next middleware
await next()
// Do something after downstream handlers
console.log('After')
}
Basic Plugin Structure
A simple plugin is a function that returns middleware:
import { Middleware } from 'grammy'
function myPlugin(): Middleware {
return async (ctx, next) => {
// Plugin logic
await next()
}
}
// Usage
bot.use(myPlugin())
Plugin with Configuration
Most plugins accept configuration options:
interface MyPluginOptions {
prefix?: string
enabled?: boolean
}
function myPlugin(options: MyPluginOptions = {}): Middleware {
const { prefix = 'bot', enabled = true } = options
return async (ctx, next) => {
if (!enabled) return next()
console.log(`[${prefix}] Processing update`)
await next()
}
}
// Usage
bot.use(myPlugin({ prefix: 'my-bot', enabled: true }))
Adding Context Properties
Plugins often extend the context object with new properties or methods.
Defining a Context Flavor
import { Context } from 'grammy'
// Define what your plugin adds to context
interface MyPluginFlavor {
myFeature: {
doSomething: () => void
getValue: () => string
}
}
// Create a flavored context type
type MyContext = Context & MyPluginFlavor
Implementing the Plugin
import { Middleware } from 'grammy'
function myPlugin(): Middleware<MyContext> {
return (ctx, next) => {
// Add properties to context
ctx.myFeature = {
doSomething() {
console.log('Doing something!')
},
getValue() {
return 'Hello from plugin'
},
}
return next()
}
}
Using the Flavored Plugin
import { Bot } from 'grammy'
const bot = new Bot<MyContext>('YOUR_BOT_TOKEN')
bot.use(myPlugin())
bot.on('message', (ctx) => {
// TypeScript knows about myFeature
ctx.myFeature.doSomething()
const value = ctx.myFeature.getValue()
ctx.reply(value)
})
Using the Composer Class
The Composer class lets you build complex middleware systems:
import { Composer } from 'grammy'
function featurePlugin() {
const composer = new Composer()
composer.command('start', (ctx) => {
return ctx.reply('Started!')
})
composer.command('help', (ctx) => {
return ctx.reply('Help text')
})
composer.on('message:text', (ctx) => {
console.log('Text message:', ctx.message.text)
return next()
})
return composer
}
// Usage
bot.use(featurePlugin())
Middleware Object Pattern
Implement the MiddlewareObj interface for advanced plugins:
import { MiddlewareObj, MiddlewareFn } from 'grammy'
class MyPlugin implements MiddlewareObj {
private config: Record<string, any>
constructor(options = {}) {
this.config = options
}
middleware(): MiddlewareFn {
return async (ctx, next) => {
// Access this.config
console.log('Config:', this.config)
await next()
}
}
}
// Usage
bot.use(new MyPlugin({ key: 'value' }))
Real-World Example: Logger Plugin
Here’s a complete example of a logging plugin:
import { Context, Middleware } from 'grammy'
interface LoggerOptions {
logUpdates?: boolean
logErrors?: boolean
prefix?: string
}
function logger(options: LoggerOptions = {}): Middleware {
const {
logUpdates = true,
logErrors = true,
prefix = 'BOT',
} = options
return async (ctx, next) => {
const start = Date.now()
if (logUpdates) {
console.log(
`[${prefix}] Update ${ctx.update.update_id} from ${
ctx.from?.id || 'unknown'
}`
)
}
try {
await next()
} catch (error) {
if (logErrors) {
console.error(`[${prefix}] Error:`, error)
}
throw error
} finally {
const duration = Date.now() - start
console.log(`[${prefix}] Processed in ${duration}ms`)
}
}
}
// Usage
bot.use(logger({ prefix: 'MY-BOT' }))
Real-World Example: Rate Limiter
A rate limiting plugin to prevent spam:
import { Middleware } from 'grammy'
interface RateLimitOptions {
timeWindow?: number // milliseconds
limit?: number // max requests per window
}
function rateLimit(options: RateLimitOptions = {}): Middleware {
const { timeWindow = 1000, limit = 5 } = options
const requests = new Map<number, number[]>()
return async (ctx, next) => {
const userId = ctx.from?.id
if (!userId) return next()
const now = Date.now()
const userRequests = requests.get(userId) || []
// Remove old requests outside the time window
const recentRequests = userRequests.filter(
(time) => now - time < timeWindow
)
if (recentRequests.length >= limit) {
return ctx.reply('Too many requests. Please slow down.')
}
recentRequests.push(now)
requests.set(userId, recentRequests)
await next()
}
}
// Usage
bot.use(rateLimit({ timeWindow: 60000, limit: 10 }))
Real-World Example: Translation Plugin
A plugin that adds translation capabilities:
import { Context, Middleware } from 'grammy'
interface Translations {
[locale: string]: {
[key: string]: string
}
}
interface I18nFlavor {
t: (key: string) => string
locale: string
}
type I18nContext = Context & I18nFlavor
function i18n(translations: Translations): Middleware<I18nContext> {
return (ctx, next) => {
// Detect user's language (simplified)
const locale = ctx.from?.language_code || 'en'
ctx.locale = locale
ctx.t = (key: string) => {
return translations[locale]?.[key] || translations.en?.[key] || key
}
return next()
}
}
// Usage
const translations = {
en: {
welcome: 'Welcome!',
goodbye: 'Goodbye!',
},
es: {
welcome: '¡Bienvenido!',
goodbye: '¡Adiós!',
},
}
const bot = new Bot<I18nContext>('YOUR_BOT_TOKEN')
bot.use(i18n(translations))
bot.command('start', (ctx) => {
return ctx.reply(ctx.t('welcome'))
})
Advanced: Lazy Middleware
Create middleware that’s generated on-the-fly:
import { Composer } from 'grammy'
function dynamicPlugin() {
const composer = new Composer()
composer.lazy((ctx) => {
// Generate middleware based on the context
if (ctx.from?.is_premium) {
return premiumFeatures
} else {
return basicFeatures
}
})
return composer
}
Advanced: Fork and Concurrency
Run middleware concurrently:
import { Composer } from 'grammy'
function analyticsPlugin() {
const composer = new Composer()
// Fork: runs concurrently with other middleware
composer.fork(async (ctx) => {
// Log analytics without blocking the main flow
await logToAnalytics(ctx.update)
})
composer.on('message', (ctx) => {
// This runs in parallel with the fork
return ctx.reply('Got your message!')
})
return composer
}
Advanced: Error Boundaries
Create safe plugin zones:
import { Composer } from 'grammy'
function safePlugin() {
const composer = new Composer()
composer.errorBoundary(
(err) => {
console.error('Plugin error:', err)
},
riskyMiddleware1,
riskyMiddleware2
)
return composer
}
Advanced: Filter and Branch
Conditional middleware execution:
import { Composer } from 'grammy'
function conditionalPlugin() {
const composer = new Composer()
// Only run for private chats
composer.filter(
(ctx) => ctx.chat?.type === 'private',
privateHandler
)
// Branch based on condition
composer.branch(
(ctx) => ctx.from?.is_bot,
botHandler,
userHandler
)
return composer
}
Testing Your Plugin
Always test your plugins:
import { Bot, Context } from 'grammy'
// Create a test context
function createMockContext(): Context {
return {
update: { update_id: 1, message: { /* ... */ } },
api: /* mock API */,
// ... other required properties
} as any
}
// Test the plugin
async function testPlugin() {
const bot = new Bot('fake-token')
bot.use(myPlugin())
const ctx = createMockContext()
await bot.handleUpdate(ctx.update)
// Assert expected behavior
}
Best Practices
- Type Safety - Always define context flavors for TypeScript support
- Configuration - Accept options to make plugins flexible
- Error Handling - Handle errors gracefully, don’t crash the bot
- Documentation - Document your plugin’s API and usage
- Testing - Write tests for your plugin logic
- Performance - Avoid blocking operations in middleware
- Composability - Make plugins work well with others
- Naming - Use clear, descriptive names for your plugins
Publishing Your Plugin
To share your plugin with the community:
- Create a separate npm package
- Use clear naming:
grammy-plugin-name or @yourscope/grammy-plugin-name
- Include TypeScript types
- Write comprehensive documentation
- Add examples and tests
- Publish to npm
{
"name": "grammy-plugin-example",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"peerDependencies": {
"grammy": "^1.0.0"
}
}
Example: Complete Plugin Template
import { Context, Middleware } from 'grammy'
// 1. Define the context flavor
export interface MyPluginFlavor {
myPlugin: {
// Add your methods here
}
}
export type MyPluginContext = Context & MyPluginFlavor
// 2. Define options
export interface MyPluginOptions {
// Add configuration options
}
// 3. Implement the plugin
export function myPlugin(
options: MyPluginOptions = {}
): Middleware<MyPluginContext> {
return (ctx, next) => {
// Add functionality to context
ctx.myPlugin = {
// Implement methods
}
return next()
}
}
// 4. Export everything
export default myPlugin
Next Steps