Quick Start¶
From zero to an authenticated, error-handled API call. Every snippet is copy-pasteable.
Table of Contents¶
- Requirements
- 1. Point the @msflib scope at GitHub Packages
- 2. Install
- 3. Create one API client for your app
- 4. Log in and call an authenticated endpoint
- 5. Show errors to the user
- 6. Upload a file
- 7. Grab a utility
- Common gotchas
- Using the msflib React modules?
Requirements¶
- Any modern browser, or Node 20+ (the library relies on global
fetch,FormDataandFile). - A GitHub account with access to the
msfliborganisation'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