@mustafaaksoy41/sharepoint-kit

v0.1.13

Published

Full-featured SharePoint Graph API client, React hooks, Radix UI components, and CLI type generator for TypeScript projects

Readme


Why SharePoint Kit?

Built to solve type safety, boilerplate, and auth complexity when connecting to SharePoint via the Graph API. Everything in one package:

  • Graph API client – CRUD with retry and throttle handling
  • React hooks – SWR-based useSpList, useSpItem, useSpCreate, and more
  • Radix UI components – Ready-to-use <SpListTable>, <SpItemForm>, <SpItemCard>
  • CLI type generator – Generate TypeScript types from SharePoint content types
  • Config-driven – Each project defines its SharePoint structure in sharepoint.config.ts

Installation

npm install @mustafaaksoy41/sharepoint-kit

Peer dependencies

npm install react react-dom swr @radix-ui/themes @radix-ui/react-icons

Quick Start

1. Environment variables

Create a .env file in your project root (the CLI loads it automatically via dotenv):

SHAREPOINT_SITE_ID=root
SHAREPOINT_TENANT_ID=your-tenant-id
SHAREPOINT_CLIENT_ID=your-client-id
SHAREPOINT_CLIENT_SECRET=your-client-secret

2. Config file

Create sharepoint.config.ts (or sharepoint.config.json):

export default {
  siteId: process.env.SHAREPOINT_SITE_ID || 'root',
  tenantId: process.env.SHAREPOINT_TENANT_ID,
  clientId: process.env.SHAREPOINT_CLIENT_ID,
  clientSecret: process.env.SHAREPOINT_CLIENT_SECRET,
  defaultStrategy: 'interactive',
  contentTypes: [
    {
      listId: '50fc630f-3495-4fc1-81e4-dfa7ef915574',  // use listId for document libraries
      contentTypeName: 'Fatura Denemesi',
      outputType: 'Invoice',
    },
    {
      contentTypeName: 'Belge',
      outputType: 'Document',
    },
  ],
  lists: [
    {
      listId: '12345678-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
      outputType: 'Employee',
    },
    {
      listName: 'Depo Stok Durumu',
      outputType: 'InventoryItem',
    }
  ],
  options: {
    outputDir: './generated',
    fieldNameMapping: {
      'Fatura_x0020_Numaras_x0131_': 'faturaNo',
      'M_x00fc__x015f_teri_x0020_No': 'musteriNo',
    },
  },
};

3. Run the type generator

npx sp-generate-types -c sharepoint.config.ts

For CI/CD (non-interactive):

npx sp-generate-types -c sharepoint.config.ts --non-interactive

Demo Terminal

SharePoint Kit type generator CLI output

This generates ./generated/sp-types.ts:

export interface Invoice {
  faturaNo?: string;
  musteriNo?: string;
  Tutar?: number;
  Title?: string;
}

export interface Document {
  Title?: string;
}

4. Why fieldNameMapping?

SharePoint returns column InternalNames in an encoded format. Spaces and special characters become hex codes:

| Display name | InternalName (from Graph API) | |--------------|------------------------------| | Fatura Numarası | Fatura_x0020_Numaras_x0131_ | | Müşteri No | M_x00fc__x015f_teri_x0020_No |

Without mapping, you'd get ugly property names in TypeScript. The mapping converts them to camelCase:

fieldNameMapping: {
  'Fatura_x0020_Numaras_x0131_': 'faturaNo',
  'M_x00fc__x015f_teri_x0020_No': 'musteriNo',
}

Important: The mapping key must match the InternalName exactly (including typos like Np vs No). Check the generated file or Graph API response if a field isn't being mapped.

5. listId vs listName

For document libraries (e.g. Shared Documents / Belgeler), listId is more reliable. The Graph API getLists() result can vary by tenant or locale. If you know the list ID from the URL (/sites/root/lists/{list-id}/items), use it directly:

{ listId: '50fc630f-3495-4fc1-81e4-dfa7ef915574', contentTypeName: 'Fatura Denemesi', outputType: 'Invoice' }

6. Use the data client

import { createSpClient } from '@mustafaaksoy41/sharepoint-kit';
import type { Invoice, Employee } from './generated/sp-types';

const client = createSpClient({
  siteId: 'root',
  getAccessToken: async () => yourTokenFunction(),
});

// CRUD with full type safety (via Content Type)
const items = await client.getListItems<Invoice>({
  listId: '50fc630f-...',
  contentTypeName: 'Invoice',
});

// CRUD with full type safety (via Direct List)
const employees = await client.getListItems<Employee>({
  listId: '12345678-...',
});

const item = await client.getItem<Invoice>({ listId: '...', itemId: '6' });
const created = await client.createItem<Invoice>({ listId: '...', fields: { Amount: 500 } });
const updated = await client.updateItem<Invoice>({ listId: '...', itemId: '6', fields: { Amount: 600 } });
await client.deleteItem({ listId: '...', itemId: '6' });

7. Use React hooks

import { SpProvider } from '@mustafaaksoy41/sharepoint-kit/components';
import { useSpList, useSpItem, useSpCreate } from '@mustafaaksoy41/sharepoint-kit/hooks';
import type { Invoice } from './generated/sp-types';

function App() {
  return (
    <SpProvider config={{ siteId: 'root', getAccessToken }}>
      <InvoiceList />
    </SpProvider>
  );
}

function InvoiceList() {
  const { data, isLoading, error } = useSpList<Invoice>({
    listId: '50fc630f-...',
    contentTypeName: 'Invoice',
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {data?.map((item) => (
        <li key={item.id}>{item.fields.Title} - {item.fields.Amount}</li>
      ))}
    </ul>
  );
}

8. Radix UI components

import { SpProvider, SpListTable, SpItemForm, SpErrorBoundary } from '@mustafaaksoy41/sharepoint-kit/components';
import type { Invoice } from './generated/sp-types';

function App() {
  return (
    <SpProvider config={{ siteId: 'root', getAccessToken }}>
      <SpErrorBoundary onAuthError={() => router.push('/login')}>
        <SpListTable<Invoice>
          listId="50fc630f-..."
          contentTypeName="Invoice"
          columns={[
            { key: 'invoiceNo', label: 'Invoice No' },
            { key: 'Amount', label: 'Amount', format: 'currency' },
          ]}
          onRowClick={(item, id) => router.push(`/invoices/${id}`)}
        />
      </SpErrorBoundary>
    </SpProvider>
  );
}

Authentication

You can use Microsoft Login (recommended) or manual token / bypass.

Microsoft Login

Add to your .env (use NEXT_PUBLIC_ so the browser can read them):

NEXT_PUBLIC_SHAREPOINT_TENANT_ID=your-tenant-id
NEXT_PUBLIC_SHAREPOINT_CLIENT_ID=your-client-id
NEXT_PUBLIC_SHAREPOINT_REDIRECT_URI=http://localhost:3000

In Azure AD: App Registration → Authentication → add SPA with this Redirect URI. API permissions: Sites.Read.All (Delegated).

Wrap your app with SpAuthProvider and loginConfig:

import { SpAuthProvider, SpProviderWithAuth, type SpLoginConfig } from '@mustafaaksoy41/sharepoint-kit/components';
import { Theme } from '@radix-ui/themes';

const loginConfig: SpLoginConfig = {
  tenantId: process.env.NEXT_PUBLIC_SHAREPOINT_TENANT_ID!,
  clientId: process.env.NEXT_PUBLIC_SHAREPOINT_CLIENT_ID!,
  redirectUri: process.env.NEXT_PUBLIC_SHAREPOINT_REDIRECT_URI!,
};

<Theme>
  <SpAuthProvider loginConfig={loginConfig}>
    <SpProviderWithAuth siteId="root">
      {/* Your app */}
    </SpProviderWithAuth>
  </SpAuthProvider>
</Theme>

Users sign in with "Sign in with Microsoft"; the token is stored and used for Graph API calls.

Manual token or bypass (development)

<SpAuthProvider bypassEnabled={true}>
  <SpProviderWithAuth siteId="root">
    {/* Your app */}
  </SpProviderWithAuth>
</SpAuthProvider>

CLI usage

# Interactive mode (default)
npx sp-generate-types --config sharepoint.config.ts

# Non-interactive for CI/CD
npx sp-generate-types --config sharepoint.config.ts --non-interactive

# With strategy
npx sp-generate-types --config sharepoint.config.ts --non-interactive --strategy first
# Strategies: interactive | first | error | all

# Clear cache
npx sp-generate-types --config sharepoint.config.ts --clear-cache

Config options

| Field | Type | Required | Description | |-------|------|----------|-------------| | siteId | string | Yes | SharePoint site ID | | tenantId | string | Yes | Azure AD tenant ID | | clientId | string | Yes | Azure AD app client ID | | defaultStrategy | string | No | Default list selection strategy | | contentTypes | array | No* | Content types to generate (either contentTypes or lists must be provided) | | lists | array | No | Direct lists to generate (*either contentTypes or lists must be provided) | | options.outputDir | string | No | Output directory (default: ./generated) | | options.fieldNameMapping | object | No | SharePoint field name → TS property mapping |

Content type config (contentTypes)

| Field | Type | Required | Description | |-------|------|----------|-------------| | contentTypeName | string | Yes | Content type name | | outputType | string | Yes | TypeScript interface name to generate | | listId | string | No | SharePoint list ID (optional) | | listName | string | No | List display name (optional) | | strategy | string | No | Override strategy for this content type |

Direct list config (lists)

| Field | Type | Required | Description | |-------|------|----------|-------------| | outputType | string | Yes | TypeScript interface name to generate | | listId | string | No* | SharePoint list ID (either listId or listName is required) | | listName | string | No | List display name (*either listId or listName is required) |

List resolution logic

For Content Types:

  1. If listId is provided, use it directly
  2. If listName is provided, find the list by name
  3. If neither is provided, scan all lists for the content type:
    • If found in one list, use it
    • If found in multiple lists, prompt the user (interactive) or apply the strategy

For Direct Lists:

  1. If listId is provided, use it directly
  2. If listName is provided, find the list by name

What the CLI does (under the hood)

  1. Loads config and fetches an access token (client credentials flow)
  2. Parses both contentTypes and lists arrays
  3. Resolves each item to a list (by listId, listName, or scanning)
  4. Calls Graph API: getListContentTypes (for content types), getColumns / getListColumns
  5. Maps column types from Graph's format (text, number, currency etc.) to TypeScript
  6. Applies fieldNameMapping to rename encoded InternalNames to camelCase
  7. Writes sp-types.ts to the output directory

Error handling

import { SpError, SpAuthError, SpNotFoundError, SpThrottleError, SpValidationError } from '@mustafaaksoy41/sharepoint-kit';

try {
  await client.getItem({ listId, itemId: '999' });
} catch (error) {
  if (error instanceof SpAuthError) {
    // 401/403 - redirect to login
  } else if (error instanceof SpNotFoundError) {
    // 404 - item not found
  } else if (error instanceof SpThrottleError) {
    // 429 - retryAfter available
    console.log(`Retry after ${error.retryAfter} seconds`);
  } else if (error instanceof SpValidationError) {
    // 400 - validation errors
    console.log(error.fieldErrors);
  }
}

Type mapping

The Microsoft Graph API returns column definitions with properties like text, number, currency rather than a single type string. These map to TypeScript as follows:

| Graph API / SharePoint Type | TypeScript Type | |-----------------------------|-----------------| | text, text (multiline), choice | string | | multiSelect | string[] | | number, currency | number | | boolean | boolean | | dateTime | string (ISO) | | lookup | { id: number; value: string } | | personOrGroup | { id: number; email: string; displayName: string } | | url | { url: string; description: string } |


License

MIT