TypeScriptUI Widgets

Widget Manifest

Widget manifest format

Learn how to use the Widget Manifest system for independent frontend development and UI testing without needing a running backend.

The Widget Manifest system enables independent frontend development by providing example data for UI widgets. Frontend developers can preview and test widgets in Studio's Resources tab without needing a live backend or tool execution.

What is a Widget Manifest?

A Widget Manifest is a JSON file (widget-manifest.json) that:

  • Lists all UI widgets in your project
  • Provides example data for each widget
  • Includes metadata like names, descriptions, and tags
  • Enables live previews in NitroStack Studio

It's located at: src/widgets/widget-manifest.json

Why Use Widget Manifests?

A manifest lets widgets be designed and previewed before the tools that feed them exist.

For Frontend Developers

Work Independently - Develop UI without waiting for backend
Instant Previews - See widgets render with realistic data
Multiple Examples - Test different UI states
No API Needed - Preview without running the MCP server

For Teams

Parallel Development - Frontend and backend teams work simultaneously
Better Communication - Shared understanding of data structures
Faster Iteration - Quick UI feedback loop
Documentation - Self-documenting widget examples

Manifest Structure

The manifest is one JSON file with a list of widgets and their example data.

Basic Format

A minimal manifest with one widget and one example:

{
  "version": "1.0.0",
  "widgets": [
    {
      "uri": "/widget-name",
      "name": "Display Name",
      "description": "What this widget does",
      "examples": [
        {
          "name": "Example Name",
          "description": "Example description",
          "data": {
            "field1": "value1",
            "field2": "value2"
          }
        }
      ],
      "tags": ["category", "type"]
    }
  ],
  "generatedAt": "2025-01-24T00:00:00.000Z"
}

Field Descriptions

Top-level fields:

FieldTypeRequiredDescription
versionstringYesManifest version (e.g., "1.0.0")
widgetsarrayYesArray of widget definitions
generatedAtstringNoISO timestamp of generation

Widget Definition

Fields for each entry in widgets:

FieldTypeRequiredDescription
uristringYesWidget route (e.g., "/product-card")
namestringYesDisplay name for Studio
descriptionstringYesWhat the widget displays
examplesarrayYesArray of example data objects
tagsarrayNoCategorization tags

Example Object

Fields for each entry in a widget's examples:

FieldTypeRequiredDescription
namestringYesExample name (shown in dropdown)
descriptionstringNoExample description
dataobjectYesThe actual data passed to widget

Creating a Widget Manifest

Build the manifest from the examples you already have in your tools.

Step 1: Extract Tool Examples

Look at your tool definitions and extract the example responses:

// auth.tools.ts
@Tool({
  name: 'login',
  examples: {
    request: { email: 'user@example.com', password: 'pass123' },
    response: {
      message: 'Login successful!',
      token: 'jwt_token_here',
      user: {
        id: 'user-1',
        email: 'user@example.com',
        name: 'John Doe'
      }
    }
  }
})
@Widget('login-result')
async login(input: any) { ... }

Step 2: Create Widget Entry

Use the response from examples as the data field:

{
  "uri": "/login-result",
  "name": "Login Result",
  "description": "Displays login success with user info and JWT token",
  "examples": [
    {
      "name": "Successful Login",
      "description": "Login for John Doe",
      "data": {
        "message": "Login successful!",
        "token": "jwt_token_here",
        "user": {
          "id": "user-1",
          "email": "user@example.com",
          "name": "John Doe"
        }
      }
    }
  ],
  "tags": ["auth", "login"]
}

Step 3: Add Multiple Examples (Optional)

Provide different UI states:

{
  "uri": "/product-card",
  "name": "Product Card",
  "description": "Displays product details",
  "examples": [
    {
      "name": "In Stock Product",
      "description": "Product with high stock",
      "data": {
        "product": { "name": "Laptop", "price": 999, "stock": 50 },
        "availability": "In Stock"
      }
    },
    {
      "name": "Low Stock Product",
      "description": "Product with low stock",
      "data": {
        "product": { "name": "Laptop", "price": 999, "stock": 2 },
        "availability": "In Stock",
        "stockMessage": "Only 2 left!"
      }
    },
    {
      "name": "Out of Stock",
      "description": "Unavailable product",
      "data": {
        "product": { "name": "Laptop", "price": 999, "stock": 0 },
        "availability": "Out of Stock"
      }
    }
  ],
  "tags": ["products", "details"]
}

Step 4: Save the Manifest

Place the file at: src/widgets/widget-manifest.json

Viewing widgets in Studio

The server reads the manifest when it starts and publishes it as the widget://examples resource, so you can check it in NitroStudio.

  1. Start the project with npm run dev, and open its folder in NitroStudio.
  2. Open Resources and select Widget Examples (widget://examples). The JSON shows every widget in the manifest with its examples.
  3. Open Tools and run a tool linked to a widget. When the tool has no example of its own, Studio renders the widget with the manifest's example data.

The manifest is read once at startup, so restart npm run dev after you change it.

Example Workflow

Here's the whole process for one new widget.

Scenario: Adding a New Widget

Let's add a "User Profile" widget to an e-commerce app.

1. Create the Widget Component

Create the widget's page under src/widgets/app/.

// src/widgets/app/user-profile/page.tsx
'use client';

export default function UserProfile({ data }: { data: any }) {
  return (
    <div className="p-6 bg-white rounded-lg">
      <img 
        src={data.user.avatar} 
        alt={data.user.name}
        className="w-24 h-24 rounded-full"
      />
      <h2 className="text-2xl font-bold mt-4">{data.user.name}</h2>
      <p className="text-gray-600">{data.user.email}</p>
      <div className="mt-4">
        <p>Member since: {data.user.memberSince}</p>
        <p>Orders: {data.stats.totalOrders}</p>
      </div>
    </div>
  );
}

2. Define the Tool with Example

Give the tool an example response in the shape the widget expects.

// src/modules/users/users.tools.ts
@Tool({
  name: 'get_user_profile',
  description: 'Get user profile information',
  inputSchema: z.object({}),
  examples: {
    request: {},
    response: {
      user: {
        id: 'user-1',
        name: 'Emily Johnson',
        email: 'emily@example.com',
        avatar: 'https://example.com/avatar.jpg',
        memberSince: '2024-01-01'
      },
      stats: {
        totalOrders: 15,
        totalSpent: 1234.56
      }
    }
  }
})
@Widget('user-profile')
async getUserProfile(input: any, ctx: ExecutionContext) {
  // Tool implementation...
}

3. Add to Widget Manifest

Add the widget and the same example to the manifest.

{
  "uri": "/user-profile",
  "name": "User Profile",
  "description": "Displays user profile with stats",
  "examples": [
    {
      "name": "Emily's Profile",
      "description": "Profile for Emily Johnson",
      "data": {
        "user": {
          "id": "user-1",
          "name": "Emily Johnson",
          "email": "emily@example.com",
          "avatar": "https://example.com/avatar.jpg",
          "memberSince": "2024-01-01"
        },
        "stats": {
          "totalOrders": 15,
          "totalSpent": 1234.56
        }
      }
    },
    {
      "name": "New User Profile",
      "description": "Profile for a new user",
      "data": {
        "user": {
          "id": "user-2",
          "name": "John Doe",
          "email": "john@example.com",
          "avatar": "https://example.com/avatar2.jpg",
          "memberSince": "2025-01-20"
        },
        "stats": {
          "totalOrders": 0,
          "totalSpent": 0
        }
      }
    }
  ],
  "tags": ["users", "profile"]
}

4. Preview in Studio

  1. Restart dev server (rebuilds): npm run dev
  2. Open Studio → Resources tab
  3. Find "User Profile" in UI Widgets section
  4. Switch between examples using dropdown
  5. Click Enlarge for full view

5. Iterate on UI

Now you can:

  • Adjust styles in user-profile/page.tsx
  • See changes instantly with hot reload
  • Test with different example data
  • No backend needed!

Troubleshooting

If a widget doesn't appear in Studio, check these first.

Widgets Not Appearing in Studio

Problem: Resources tab doesn't show UI Widgets section

Solutions:

  1. Check manifest exists at src/widgets/widget-manifest.json
  2. Verify JSON is valid (no syntax errors)
  3. Rebuild the project: npm run build
  4. Restart dev server: npm run dev

Widget Preview Not Loading

Problem: Preview shows "No widget URI available"

Solutions:

  1. Check uri field matches your widget folder name
  2. Verify widget component exists at src/widgets/app{uri}/page.tsx
  3. Check uri starts with / (e.g., /product-card)

Wrong Data Displayed

Problem: Widget shows incorrect or malformed data

Solutions:

  1. Compare manifest data structure with tool's example response
  2. Check for typos in field names
  3. Ensure nested objects match exactly
  4. Verify data types (string vs number)

Example Selector Not Working

Problem: Dropdown doesn't update preview

Solutions:

  1. Check multiple examples exist in widget definition
  2. Ensure each example has unique name
  3. Clear browser cache and reload
  4. Check browser console for errors

Enlarge Button Not Working

Problem: Modal doesn't open or opens delayed

Solutions:

  1. Verify EnlargeModal is in root layout
  2. Check Zustand store is properly configured
  3. Try clicking once and waiting a moment
  4. Check browser console for errors

Advanced Features

For many widgets, generate the manifest instead of writing it by hand.

Dynamic Example Generation

For large datasets, you can generate examples programmatically:

// generate-manifest.js
const products = require('./data/products.json');

const manifest = {
  version: "1.0.0",
  widgets: [
    {
      uri: "/products-grid",
      name: "Products Grid",
      description: "Browse products",
      examples: [
        {
          name: "Electronics",
          data: {
            products: products.filter(p => p.category === 'Electronics').slice(0, 10)
          }
        },
        {
          name: "Clothing",
          data: {
            products: products.filter(p => p.category === 'Clothing').slice(0, 10)
          }
        }
      ],
      tags: ["products", "grid"]
    }
  ]
};

require('fs').writeFileSync(
  'src/widgets/widget-manifest.json',
  JSON.stringify(manifest, null, 2)
);

Run: node generate-manifest.js

TypeScript Type Safety

Define types for your manifest:

// widget-manifest.d.ts
export interface WidgetExample {
  name: string;
  description?: string;
  data: Record<string, any>;
}

export interface WidgetMetadata {
  uri: string;
  name: string;
  description: string;
  examples: WidgetExample[];
  tags?: string[];
}

export interface WidgetManifest {
  version: string;
  widgets: WidgetMetadata[];
  generatedAt?: string;
}

Use with defineWidgetMetadata helper (available in SDK):

import { defineWidgetMetadata } from '@nitrostack/widgets';

const widget = defineWidgetMetadata({
  uri: '/product-card',
  name: 'Product Card',
  description: 'Display product details',
  examples: [{ name: 'Example', data: {} }],
  tags: ['products']
});

Next Steps

  • Learn More: Check out the UI Widgets Guide
  • See Templates: Explore templates/typescript-pizzaz and templates/typescript-oauth for complete examples
  • Join Community: Share your widgets and get help

Need Help? If you encounter issues not covered here, please open an issue on GitHub or join our community Discord.

On this page