Skip to content
Back to Blog
1 min read

Building Teams Apps: Integration Patterns and Best Practices

I wrote “Building Teams Apps: Integration Patterns and Best Practices” to share practical, production-minded guidance on this topic.

Teams App Components

Teams apps can include:

  • Tabs (embedded web content)
  • Bots (conversational interfaces)
  • Message extensions
  • Webhooks and connectors
  • Meeting extensions

Setting Up Teams Toolkit

# Install Teams Toolkit CLI
npm install -g @microsoft/teamsfx-cli

# Create new Teams app
teamsfx new --interactive false --capability tab --programming-language typescript

# Or create with bot capability
teamsfx new --interactive false --capability bot --programming-language csharp

Tab Application

Create an embedded web application:

// src/components/Tab.tsx
import React, { useEffect, useState } from "react";
import { app, authentication } from "@microsoft/teams-js";
import { Loader, Text, Flex, Card } from "@fluentui/react-northstar";

interface UserProfile {
  displayName: string;
  email: string;
  jobTitle: string;
}

export const Tab: React.FC = () => {
  const [loading, setLoading] = useState(true);
  const [profile, setProfile] = useState<UserProfile | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const initializeTeams = async () => {
      try {
        await app.initialize();

        // Get authentication token
        const token = await authentication.getAuthToken({
          resources: ["https://graph.microsoft.com"],
        });

        // Fetch user profile
        const response = await fetch("https://graph.microsoft.com/v1.0/me", {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        });

        if (response.ok) {
          const data = await response.json();
          setProfile({
            displayName: data.displayName,
            email: data.mail || data.userPrincipalName,
            jobTitle: data.jobTitle || "Not specified",
          });
        }
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to initialize");
      } finally {
        setLoading(false);
      }
    };

    initializeTeams();
  }, []);

  if (loading) {
    return <Loader label="Loading..." />;
  }

  if (error) {
    return <Text error content={error} />;
  }

  return (
    <Flex column gap="gap.medium" padding="padding.medium">
      <Text size="larger" weight="bold" content="Welcome to the App" />
      {profile && (
        <Card>
          <Card.Header>
            <Text content={profile.displayName} weight="bold" />
          </Card.Header>
          <Card.Body>
            <Flex column gap="gap.small">
              <Text content={`Email: ${profile.email}`} />
              <Text content={`Title: ${profile.jobTitle}`} />
            </Flex>
          </Card.Body>
        </Card>
      )}
    </Flex>
  );
};

Bot Implementation

Create a conversational bot:

// Bots/TeamsBot.cs
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Teams;
using Microsoft.Bot.Schema;
using Microsoft.Bot.Schema.Teams;

public class TeamsBot : TeamsActivityHandler
{
    private readonly IConfiguration _config;
    private readonly ILogger<TeamsBot> _logger;

    public TeamsBot(IConfiguration config, ILogger<TeamsBot> logger)
    {
        _config = config;
        _logger = logger;
    }

    protected override async Task OnMessageActivityAsync(
        ITurnContext<IMessageActivity> turnContext,
        CancellationToken cancellationToken)
    {
        var text = turnContext.Activity.Text?.Trim().ToLower();

        if (string.IsNullOrEmpty(text))
        {
            await turnContext.SendActivityAsync("Please send a message.");
            return;
        }

        // Handle different commands
        var response = text switch
        {
            "help" => CreateHelpCard(),
            "status" => await GetStatusCard(turnContext),
            _ when text.StartsWith("search ") => await SearchAsync(text[7..], turnContext),
            _ => MessageFactory.Text($"You said: {text}")
        };

        await turnContext.SendActivityAsync(response, cancellationToken);
    }

    private IMessageActivity CreateHelpCard()
    {
        var card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 4))
        {
            Body = new List<AdaptiveElement>
            {
                new AdaptiveTextBlock
                {
                    Text = "Available Commands",
                    Weight = AdaptiveTextWeight.Bolder,
                    Size = AdaptiveTextSize.Large
                },
                new AdaptiveFactSet
                {
                    Facts = new List<AdaptiveFact>
                    {
                        new("help", "Show this help message"),
                        new("status", "Check system status"),
                        new("search <query>", "Search for items")
                    }
                }
            }
        };

        var attachment = new Attachment
        {
            ContentType = AdaptiveCard.ContentType,
            Content = card
        };

        return MessageFactory.Attachment(attachment);
    }

    private async Task<IMessageActivity> GetStatusCard(ITurnContext context)
    {
        // Fetch status from your API
        var status = new
        {
            Api = "Healthy",
            Database = "Healthy",
            LastUpdated = DateTime.UtcNow
        };

        var card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 4))
        {
            Body = new List<AdaptiveElement>
            {
                new AdaptiveTextBlock
                {
                    Text = "System Status",
                    Weight = AdaptiveTextWeight.Bolder
                },
                new AdaptiveColumnSet
                {
                    Columns = new List<AdaptiveColumn>
                    {
                        new AdaptiveColumn
                        {
                            Items = new List<AdaptiveElement>
                            {
                                new AdaptiveTextBlock { Text = "API" },
                                new AdaptiveTextBlock { Text = "Database" }
                            }
                        },
                        new AdaptiveColumn
                        {
                            Items = new List<AdaptiveElement>
                            {
                                new AdaptiveTextBlock
                                {
                                    Text = status.Api,
                                    Color = AdaptiveTextColor.Good
                                },
                                new AdaptiveTextBlock
                                {
                                    Text = status.Database,
                                    Color = AdaptiveTextColor.Good
                                }
                            }
                        }
                    }
                }
            }
        };

        return MessageFactory.Attachment(new Attachment
        {
            ContentType = AdaptiveCard.ContentType,
            Content = card
        });
    }

    private async Task<IMessageActivity> SearchAsync(string query, ITurnContext context)
    {
        // Implement search logic
        return MessageFactory.Text($"Searching for: {query}");
    }

    protected override async Task OnTeamsMembersAddedAsync(
        IList<TeamsChannelAccount> membersAdded,
        TeamInfo teamInfo,
        ITurnContext<IConversationUpdateActivity> turnContext,
        CancellationToken cancellationToken)
    {
        foreach (var member in membersAdded)
        {
            if (member.Id != turnContext.Activity.Recipient.Id)
            {
                await turnContext.SendActivityAsync(
                    $"Welcome {member.Name}! Type 'help' to see available commands.",
                    cancellationToken: cancellationToken);
            }
        }
    }
}

Message Extension

Create a search-based message extension:

// Bots/MessageExtension.cs
public class MessageExtension : TeamsActivityHandler
{
    protected override async Task<MessagingExtensionResponse> OnTeamsMessagingExtensionQueryAsync(
        ITurnContext<IInvokeActivity> turnContext,
        MessagingExtensionQuery query,
        CancellationToken cancellationToken)
    {
        var searchText = query.Parameters?.FirstOrDefault(p => p.Name == "search")?.Value as string ?? "";

        // Search your data source
        var results = await SearchItemsAsync(searchText);

        var attachments = results.Select(item => new MessagingExtensionAttachment
        {
            ContentType = HeroCard.ContentType,
            Content = new HeroCard
            {
                Title = item.Title,
                Subtitle = item.Description,
                Images = new List<CardImage>
                {
                    new CardImage(item.ImageUrl)
                }
            },
            Preview = new HeroCard
            {
                Title = item.Title,
                Tap = new CardAction
                {
                    Type = ActionTypes.InvokeAction,
                    Value = item
                }
            }.ToAttachment()
        }).ToList();

        return new MessagingExtensionResponse
        {
            ComposeExtension = new MessagingExtensionResult
            {
                Type = "result",
                AttachmentLayout = "list",
                Attachments = attachments
            }
        };
    }

    protected override async Task<MessagingExtensionActionResponse> OnTeamsMessagingExtensionSubmitActionAsync(
        ITurnContext<IInvokeActivity> turnContext,
        MessagingExtensionAction action,
        CancellationToken cancellationToken)
    {
        var data = action.Data as JObject;
        var title = data?["title"]?.ToString();
        var description = data?["description"]?.ToString();

        var card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 4))
        {
            Body = new List<AdaptiveElement>
            {
                new AdaptiveTextBlock
                {
                    Text = title,
                    Weight = AdaptiveTextWeight.Bolder,
                    Size = AdaptiveTextSize.Large
                },
                new AdaptiveTextBlock
                {
                    Text = description,
                    Wrap = true
                }
            }
        };

        return new MessagingExtensionActionResponse
        {
            ComposeExtension = new MessagingExtensionResult
            {
                Type = "result",
                AttachmentLayout = "list",
                Attachments = new List<MessagingExtensionAttachment>
                {
                    new MessagingExtensionAttachment
                    {
                        ContentType = AdaptiveCard.ContentType,
                        Content = card
                    }
                }
            }
        };
    }

    private async Task<List<SearchItem>> SearchItemsAsync(string query)
    {
        // Implement your search logic
        return new List<SearchItem>
        {
            new("Item 1", "Description for item 1", "https://example.com/image1.png"),
            new("Item 2", "Description for item 2", "https://example.com/image2.png")
        };
    }
}

public record SearchItem(string Title, string Description, string ImageUrl);

App Manifest

Configure your Teams app:

{
  "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.14/MicrosoftTeams.schema.json",
  "manifestVersion": "1.14",
  "version": "1.0.0",
  "id": "{{APP_ID}}",
  "packageName": "com.example.teamsapp",
  "developer": {
    "name": "Contoso",
    "websiteUrl": "https://example.com",
    "privacyUrl": "https://example.com/privacy",
    "termsOfUseUrl": "https://example.com/terms"
  },
  "name": {
    "short": "My Teams App",
    "full": "My Teams App - Full Name"
  },
  "description": {
    "short": "Short description",
    "full": "Full description of the app"
  },
  "icons": {
    "color": "color.png",
    "outline": "outline.png"
  },
  "accentColor": "#FFFFFF",
  "staticTabs": [
    {
      "entityId": "home",
      "name": "Home",
      "contentUrl": "https://{{HOSTNAME}}/tab",
      "scopes": ["personal"]
    }
  ],
  "bots": [
    {
      "botId": "{{BOT_ID}}",
      "scopes": ["personal", "team", "groupchat"],
      "commandLists": [
        {
          "scopes": ["personal"],
          "commands": [
            {
              "title": "help",
              "description": "Show help"
            },
            {
              "title": "status",
              "description": "Check status"
            }
          ]
        }
      ]
    }
  ],
  "composeExtensions": [
    {
      "botId": "{{BOT_ID}}",
      "commands": [
        {
          "id": "searchQuery",
          "type": "query",
          "title": "Search",
          "description": "Search for items",
          "parameters": [
            {
              "name": "search",
              "title": "Search",
              "description": "Enter search terms"
            }
          ]
        }
      ]
    }
  ],
  "permissions": ["identity", "messageTeamMembers"],
  "validDomains": ["{{HOSTNAME}}"]
}

Summary

Teams app development offers:

  • Multiple extension points (tabs, bots, extensions)
  • Rich adaptive card experiences
  • Deep integration with Microsoft 365
  • SSO with Azure AD
  • Comprehensive SDK support

Build collaborative experiences that meet users where they work.

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.