Skip to content

Quick Start

From zero to an authenticated, error-handled API call. Every snippet is copy-pasteable.


Table of Contents


Requirements

  • Any modern browser, or Node 20+ (the library relies on global fetch, FormData and File).
  • A GitHub account with access to the msflib organisation's packages.
  • ESM (import) and CommonJS (require) are both supported, and TypeScript types are bundled.

1. Point the @msflib scope at GitHub Packages

@msflib/typescript is published to GitHub Packages, not the public npm registry, so a plain npm install will fail with a 404/401 until you do this once.

a) Create a token. On GitHub go to Settings → Developer settings → Personal access tokens → Tokens (classic), and create a classic token with the read:packages scope.

b) Export it in your shell (or add it to your shell profile):

export GITHUB_TOKEN=ghp_your_token_here

c) Add an .npmrc at the root of your project:

@msflib:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}

Warning

Never commit a literal token in .npmrc - keep the ${GITHUB_TOKEN} placeholder and supply the value from the environment.


2. Install

pnpm add @msflib/typescript
# or
npm install @msflib/typescript
# or
yarn add @msflib/typescript

3. Create one API client for your app

Create it once and import it everywhere.

// src/api.ts
import { createApiClient } from '@msflib/typescript';

export const { apiClient, apiFormDataClient, loginClient } = createApiClient({
  baseURL: 'https://api.example.com', // your backend URL, no trailing slash
  getAccessToken: () => localStorage.getItem('access_token'),
});

getAccessToken is called on every request, so once a token is in localStorage, every apiClient call sends Authorization: Bearer <token> automatically.


4. Log in and call an authenticated endpoint

loginClient posts username/password as application/x-www-form-urlencoded - the format FastAPI's OAuth2 password flow expects.

import { apiClient, loginClient } from './api';

type LoginResponse = { access_token: string; token_type: string };
type Me = { id: number; email: string };

const { access_token } = await loginClient<LoginResponse>('/login', {
  username: 'ada@example.com',
  password: 'secret',
});
localStorage.setItem('access_token', access_token);

const me = await apiClient<Me>('GET', '/me');
console.log(me.email);

// Create something (JSON body)
await apiClient('POST', '/notes', { title: 'Hello hackathon' });

// Query parameters
const page = await apiClient('GET', '/notes', null, { query: { page: 1, tag: ['a', 'b'] } });
// -> GET /notes?page=1&tag=a&tag=b

Adjust the endpoint paths and response types to your backend.


5. Show errors to the user

Everything the client throws for a failed request is a UserError whose message is safe to display. Non-2xx responses are the subclass ApiResponseError, which also carries status and per-field errors.

import { ApiResponseError, UserError } from '@msflib/typescript';
import { apiClient } from './api';

try {
  await apiClient('POST', '/signup', { email: 'taken@example.com', password: 'x' });
} catch (error) {
  if (error instanceof ApiResponseError) {
    console.log(error.status);  // e.g. 422
    console.log(error.message); // e.g. "Email already taken."
    console.log(error.errors);  // e.g. { email: ["Email already taken."] }
  } else if (error instanceof UserError) {
    console.log(error.message); // "Network error. Try again later."
  } else {
    throw error; // not from the client - e.g. a non-JSON success body
  }
}

FastAPI ({ detail }), Laravel-style ({ message, errors }) and generic ({ error }) error bodies are understood out of the box. Details: Errors.


6. Upload a file

import { asFormData } from '@msflib/typescript';
import { apiFormDataClient } from './api';

const fileInput = document.querySelector<HTMLInputElement>('#avatar')!;

await apiFormDataClient(
  'POST',
  '/uploads',
  asFormData({ title: 'My avatar', file: fileInput.files![0] }),
);

7. Grab a utility

import { debounce, omit, pick, slugify, toRecord } from '@msflib/typescript';

slugify('Team Rocket & Friends!');              // "team-rocket-friends"
omit({ id: 1, password: 'x' }, ['password']);   // { id: 1 }
pick({ id: 1, name: 'A', age: 3 }, ['id']);     // { id: 1 }
toRecord([{ id: 7, name: 'A' }]);               // { "7": { id: 7, name: 'A' } }

const onSearch = debounce((q: string) => console.log('search', q), 300);

Full list: Object · Function · String.


Common gotchas

Symptom Cause Fix
404/401 on pnpm add Registry/token not configured Do step 1
Server receives an empty body Lowercase method ('post') or 'DELETE' with apiClient Use 'POST'/'PUT'/'PATCH' (uppercase) - only these send a JSON body
SyntaxError: Unexpected end of JSON input Endpoint returned 204 No Content (or non-JSON) Return JSON from the endpoint, or catch the SyntaxError for that call
Custom header missing Passed a Headers instance in options.headers Pass a plain object
5xx shows "Could not process request..." By design Real message is on error.cause.message
ReferenceError: File is not defined asFormData on Node 18 Use Node 20+

Using the msflib React modules?

If you are using the @msflib/* React packages, @msflib/core already builds a client from this library for you (its configuredApiClient, configured once via configureApplication). Use that instead of calling createApiClient yourself, and use this library directly for its utilities and error classes.


Next: Overview · HTTP client · Errors