Skip to content

Node.js (TypeScript) โ€‹

๐Ÿ“ฆ botas-core ยท botas-express ยท ๐Ÿ“— API Reference: botas-core ยท botas-express

Installation โ€‹

The Node.js implementation ships as two npm packages, both published as ES modules and requiring Node.js 20+:

Package ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย ย Description
botas-expressAll-in-one package โ€” includes BotApp, re-exports core types
botas-coreStandalone core library โ€” use when integrating with Hono or other frameworks
bash
npm install botas-express          # recommended โ€” includes botas-core
# or, for advanced/non-Express setups:
npm install botas-core

If you're working inside the monorepo, the workspace already links the package:

bash
cd node
npm install          # installs all workspace dependencies
npm run build        # compiles the library and samples

Quick start with BotApp โ€‹

The simplest way to create a bot in Node.js is to use BotApp from the botas-express package. It sets up Express, JWT authentication, and the /api/messages endpoint in a single call:

typescript
import { BotApp } from 'botas-express'

const app = new BotApp()

app.on('message', async (ctx) => {
  await ctx.send(`You said: ${ctx.activity.text}`)
})

app.start()

That's it โ€” 8 lines to go from zero to a working bot.

What BotApp does โ€‹

Under the hood, BotApp:

  1. Creates an Express server
  2. Registers POST /api/messages with JWT authentication middleware (botAuthExpress())
  3. Wires up BotApplication.processAsync(req, res) to handle incoming activities
  4. Starts the server on process.env.PORT ?? 3978

Handler registration with app.on() โ€‹

Use app.on(type, handler) to register per-activity-type handlers. The handler receives a TurnContext (not a raw CoreActivity):

typescript
app.on('message', async (ctx) => {
  // ctx.activity is the incoming activity
  // ctx.send() sends a reply
  await ctx.send(`You said: ${ctx.activity.text}`)
})

If no handler is registered for an incoming activity type, the activity is silently ignored โ€” no error is thrown.

Sending replies with ctx.send() โ€‹

TurnContext.send() is the simplest way to send a reply:

typescript
// Send text
await ctx.send('Hello!')

// Send a full activity
await ctx.send({
  type: 'message',
  text: 'Hello!',
  conversation: ctx.activity.conversation,
  serviceUrl: ctx.activity.serviceUrl,
})

send(string) automatically creates a properly-addressed reply with the given text. send(Partial<CoreActivity>) sends the activity as-is through the authenticated ConversationClient.


Advanced: Manual framework integration โ€‹

For advanced scenarios โ€” using Hono, custom Express middleware, or other frameworks โ€” you can use BotApplication directly and wire up the HTTP handling yourself.


BotApplication โ€‹

BotApplication is the central class that processes incoming activities. It is web-framework-agnostic โ€” you wire it into Express, Hono, or any HTTP server you like.

Creating an instance โ€‹

ts
import { BotApplication } from 'botas-core'

const bot = new BotApplication()

Credentials are resolved automatically from environment variables (CLIENT_ID, CLIENT_SECRET, TENANT_ID). You can also pass them explicitly:

ts
const bot = new BotApplication({
  clientId: '00000000-0000-0000-0000-000000000000',
  clientSecret: 'your-secret',
  tenantId: 'your-tenant-id',
})

Registering activity handlers (BotApplication) โ€‹

When using BotApplication directly (not BotApp), use on(type, handler) to register an async handler for a specific activity type. Only one handler per type is supported โ€” registering the same type again replaces the previous handler. The method returns this, so you can chain calls.

The handler receives a TurnContext:

typescript
bot.on('message', async (ctx) => {
  await ctx.send(`You said: ${ctx.activity.text}`)
})

If no handler is registered for an incoming activity type, the activity is silently ignored โ€” no error is thrown.

The ActivityType type alias provides compile-time safety for known activity types:

ts
import type { ActivityType } from 'botas-core'

// ActivityType = 'message' | 'typing' | 'invoke'
bot.on('message', async (ctx) => { /* ... */ })
bot.on('typing', async (ctx) => { /* ... */ })

For Teams-specific activity types, use TeamsActivityType:

ts
import type { TeamsActivityType } from 'botas-core'

// Includes all core types plus: 'conversationUpdate', 'event', etc.
bot.on('conversationUpdate', async (ctx) => { /* ... */ })

Express integration โ€‹

The botas-express package ships a ready-made Express middleware for JWT authentication: botAuthExpress().

Wiring it up โ€‹

ts
import express from 'express'
import { BotApplication } from 'botas-core'
import { botAuthExpress } from 'botas-express'

const bot = new BotApplication()
const server = express()

server.post('/api/messages', botAuthExpress(), (req, res) => {
  bot.processAsync(req, res)
})

botAuthExpress() validates the Authorization: Bearer <token> header against the Bot Service JWKS endpoint. If validation fails, it responds with 401 before your handler ever runs.

processAsync(req, res) reads the request body, runs the middleware pipeline and handler, then writes 200 {} on success or 500 on error.


Hono integration โ€‹

For Hono, use validateBotToken and BotAuthError from botas-core to build a small auth middleware, and processBody() for activity handling. Because Hono manages its own response lifecycle, you call processBody with the raw JSON string and return the response yourself:

ts
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
import { BotApplication, validateBotToken, BotAuthError } from 'botas-core'
import type { Context, Next } from 'hono'

const bot = new BotApplication()
const app = new Hono()

function botAuthHono (appId?: string): (c: Context, next: Next) => Promise<Response | void> {
  const audience = appId ?? process.env['CLIENT_ID']
  if (!audience) throw new Error('CLIENT_ID is required')
  return async (c, next) => {
    try {
      await validateBotToken(c.req.header('authorization'), appId)
      return next()
    } catch (err) {
      if (err instanceof BotAuthError) return c.text(err.message, 401)
      throw err
    }
  }
}

app.post('/api/messages', botAuthHono(), async (c) => {
  const body = await c.req.text()
  await bot.processBody(body)
  return c.json({})
})

serve({ fetch: app.fetch, port: 3978 })

botas-core exports validateBotToken and BotAuthError so you can build auth middleware for any web framework. The Express-specific botAuthExpress() lives in botas-express for convenience.


Middleware โ€‹

Middleware lets you add cross-cutting logic (logging, telemetry, error tracking) that runs before โ€” and optionally after โ€” every activity handler.

The TurnMiddleware type โ€‹

Middleware in Node.js is a plain async function matching the TurnMiddleware type:

ts
import type { TurnMiddleware } from 'botas-express'

const loggingMiddleware: TurnMiddleware = async (context, next) => {
  console.log(`โ†’ Received ${context.activity.type}`)
  await next()  // continue to the next middleware or the handler
  console.log(`โ† Done processing ${context.activity.type}`)
}

The next callback invokes the next middleware in the chain, or the activity handler if this is the last middleware. If you don't call next(), the activity handler is skipped (short-circuit).

Registering middleware โ€‹

ts
bot.use(loggingMiddleware)

Middleware executes in registration order. The method returns this for chaining:

ts
bot
  .use(loggingMiddleware)
  .use(telemetryMiddleware)
  .on('message', async (activity) => { /* ... */ })

Error handling โ€‹

If an activity handler throws an exception, it is wrapped in a BotHandlerException that carries the original error and the triggering activity:

ts
import { BotHandlerException } from 'botas-express'

try {
  await bot.processBody(body)
} catch (err) {
  if (err instanceof BotHandlerException) {
    console.error('Handler failed for activity type:', err.activity.type)
    console.error('Original error:', err.cause)
  }
}

When using processAsync (Express), handler errors result in a 500 Internal server error response automatically.


CoreActivity schema โ€‹

CoreActivity is a plain TypeScript interface. Unknown JSON properties are captured in a properties record:

PropertyTypeDescription
typestringActivity type ("message", "typing", etc.)
serviceUrlstringThe channel's service endpoint
fromChannelAccount | undefinedSender
recipientChannelAccount | undefinedRecipient
conversationConversation | undefinedConversation reference
textstring | undefinedMessage text
entitiesEntity[] | undefinedAttached entities
attachmentsAttachment[] | undefinedAttached files/cards
propertiesRecord<string, unknown>Unknown JSON properties (preserved on round-trip)

ConversationClient โ€‹

For advanced scenarios, bot.conversationClient exposes the full Conversations REST API:

MethodDescription
sendCoreActivityAsyncSend an activity to a conversation
updateCoreActivityAsyncUpdate an existing activity
deleteCoreActivityAsyncDelete an activity
getConversationMembersAsyncList all members
getConversationPagedMembersAsyncList members with pagination
createConversationAsyncCreate a new proactive conversation

Teams features โ€‹

Use TeamsActivityBuilder to send mentions, adaptive cards, and suggested actions. See the Teams Features guide for full examples.

typescript
import { TeamsActivityBuilder } from 'botas-core'

const sender = ctx.activity.from
const reply = new TeamsActivityBuilder()
  .withConversationReference(ctx.activity)
  .withText(`<at>${sender.name}</at> said: ${ctx.activity.text}`)
  .addMention(sender)
  .build()
await ctx.send(reply)

Use TeamsActivity.fromActivity() to access Teams-specific metadata:

typescript
import { TeamsActivity } from 'botas-core'

const teamsActivity = TeamsActivity.fromActivity(ctx.activity)
const tenantId = teamsActivity.channelData?.tenant?.id

Configuration โ€‹

All credentials are read from environment variables by default:

VariableDescription
CLIENT_IDAzure AD application (bot) ID
CLIENT_SECRETAzure AD client secret
TENANT_IDAzure AD tenant ID
PORTHTTP listen port (default: 3978)

You can also pass these values through the BotApplicationOptions constructor parameter.

For setup details on Azure Bot registration and credentials, see the Setup Guide and Authentication.


Key types reference โ€‹

TypeDescription
BotApplicationMain bot class โ€” owns handlers, middleware pipeline, and send methods
CoreActivityDeserialized Bot Service activity; preserves unknown JSON properties in properties
ChannelAccountRepresents a user or bot identity (id, name, aadObjectId, role)
ConversationConversation identifier (id)
ConversationClientSends outbound activities over the authenticated HTTP client
TurnMiddlewareMiddleware function type โ€” (context, next) => Promise<void>
BotHandlerExceptionWraps handler exceptions with the triggering activity
TeamsActivityTeams-specific activity โ€” channelData, locale, suggestedActions, and fromActivity() factory
TeamsActivityBuilderFluent builder for Teams replies โ€” addMention(), addAdaptiveCardAttachment(), withSuggestedActions()
TeamsChannelDataTyped Teams channel metadata โ€” tenant, channel, team, meeting, notification
EntityActivity entity (e.g. mention)
AttachmentFile or card attachment with contentType, content

API Reference โ€‹

Full API documentation is generated with TypeDoc from TSDoc comments:

๐Ÿ“— botas-core API Reference ๐Ÿ“— botas-express API Reference

BotAS โ€” Multi-language Microsoft Teams bot library