npm package: @mngl/sanctum-spa-client
Description: Lightweight TypeScript client for Laravel Sanctum SPA XSRF protection using Axios.
Author: Michał Nagielski
Latest version: 0.4.0

# @mngl/sanctum-spa-client

[![npm version](https://badge.fury.io/js/%40mngl%2Fsanctum-spa-client.svg)](https://badge.fury.io/js/%40mngl%2Fsanctum-spa-client)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A lightweight TypeScript client for Laravel Sanctum SPA authentication with CSRF protection, designed to work seamlessly with React Router and modern frontend frameworks.

## 🚀 Features

- 🔐 **Laravel Sanctum Integration** - Full SPA authentication support
- 🛡️ **CSRF Protection** - Automatic XSRF token handling
- 🎯 **TypeScript First** - Complete type safety and IntelliSense support
- ⚡ **React Query Ready** - Perfect integration with TanStack Query
- 🔄 **Client/Server APIs** - Separate implementations for frontend and SSR
- 📦 **Lightweight** - Minimal dependencies (only Axios as peer dependency)

## 📦 Installation

```bash
npm install @mngl/sanctum-spa-client
```

## 🛠️ Setup
### Project Structure
``` 
app/
├── src/
│   └── services/
│       └── api/
│           ├── backend/
│           │   └── backendApi.server.ts
│           ├── frontend/
│           │   └── clientApi.client.ts
│           └── queries/
│               └── loginUser.ts
```
### Backend API Configuration (Server-side)
**`backendApi.server.ts`**
``` typescript
import * as process from "node:process";
import { BackendApi } from "@mngl/sanctum-spa-client";
import axios from "axios";

const axiosClient = axios.create({
	baseURL: process.env.API_BASE_URL,
	withCredentials: true,
	withXSRFToken: true,
	headers: {
		"Content-Type": "application/json",
		// Ensure this Origin matches your Sanctum configuration
		Origin: process.env.FRONTEND_URL,
	},
});

export const backendApi = new BackendApi(axiosClient);
```

### Client API Configuration (Frontend)
**`clientApi.client.ts`**
``` typescript
import { ClientApi } from "@mngl/sanctum-spa-client";
import axios from "axios";

const clientApiClient = axios.create({
	baseURL: "/api", // This should match your proxy configuration
	withCredentials: true,
	withXSRFToken: true,
	headers: {
		"Content-Type": "application/json",
	},
});

export const clientApi = new ClientApi(clientApiClient);
```
### ⚠️ Important: Proxy Configuration
When using the client API with baseURL, you **must** configure your development server to proxy API requests to your backend. Here's an example Vite configuration: `/api`
**`vite.config.ts`**
``` typescript
export default defineConfig({
	server: {
		proxy: {
			"/api": {
				target: process.env.API_BASE_URL, // e.g., "http://localhost:8000"
				rewrite: (path) => path.replace(/^\/api/, ""),
				changeOrigin: true,
			},
		},
	},
});
```
## 📚 Usage
### CSRF Protection Setup
Add CSRF protection at your root route to ensure all routes have access to the XSRF token:
**`root.tsx`**
``` typescript
import { data, type LoaderFunctionArgs } from "react-router";
import { CSRFProtection } from "@mngl/sanctum-spa-client";
import { backendApi } from "~/services/api/backend/backendApi.server";

export async function loader({ request }: LoaderFunctionArgs) {
	return data(null, {
		headers: await new CSRFProtection(backendApi).ensureToken(request),
	});
}
```
### Creating API Query Configurations
Define your API queries with proper typing:
**`queries/loginUser.ts`**
``` typescript
import type { ApiRequestConfig } from "@mngl/sanctum-spa-client";
import type { User } from "~/types/resources/user";

export type LoginData = {
	email: string;
	password: string;
};

export type LoginSuccess = User;

export const loginUser = (data: LoginData): ApiRequestConfig<LoginData> => ({
	url: "auth/login",
	method: "POST",
	data,
});
```
### Making API Requests
#### Direct Usage
``` typescript
import { clientApi } from "~/services/api/frontend/clientApi.client";
import { loginUser } from "~/services/api/queries/loginUser";

// Using request method (returns full response object)
const response = await clientApi.request(loginUser({ 
	email: "user@example.com", 
	password: "password" 
}));

if (response.success) {
	console.log("User logged in:", response.data);
} else {
	console.error("Login failed:", response.errorData);
}

// Using requestAndUnwrap (throws on error, returns data directly)
try {
	const user = await clientApi.requestAndUnwrap(loginUser({ 
		email: "user@example.com", 
		password: "password" 
	}));
	console.log("User logged in:", user);
} catch (error) {
	console.error("Login failed:", error);
}
```
### Integration with React Query
For optimal developer experience, integrate with TanStack Query:
**`hooks/useLoginMutation.ts`**
``` typescript
import { useMutation } from "@tanstack/react-query";
import { clientApi } from "~/services/api/frontend/clientApi.client";
import {
	type LoginData,
	type LoginSuccess,
	loginUser,
} from "~/services/api/queries/loginUser";

export const useLoginMutation = () =>
	useMutation<LoginSuccess, Error, LoginData>({
		mutationFn: (body) =>
			clientApi.requestAndUnwrap<LoginSuccess>(loginUser(body)),
	});
```
**Usage in React component:**
``` typescript
import { useLoginMutation } from "~/hooks/useLoginMutation";

function LoginForm() {
	const loginMutation = useLoginMutation();

	const handleSubmit = (formData: LoginData) => {
		loginMutation.mutate(formData, {
			onSuccess: (user) => {
				console.log("Welcome,", user.name);
			},
			onError: (error) => {
				console.error("Login failed:", error.message);
			},
		});
	};

	return (
		<form onSubmit={handleSubmit}>
			{/* Your form JSX */}
			<button 
				type="submit" 
				disabled={loginMutation.isPending}
			>
				{loginMutation.isPending ? "Logging in..." : "Login"}
			</button>
		</form>
	);
}
```
## 📖 API Reference
### `ClientApi`
- **`request<TData, TErrData>(config: ApiRequestConfig)`** - Makes a request and returns the full response object
- **`requestAndUnwrap<TData>(config: ApiRequestConfig)`** - Makes a request and returns only the data (throws on error)

### `BackendApi`
- **`request<TData, TErrData>(request: Request, config: ApiRequestConfig)`** - Server-side request method that accepts Web API Request object

### `CSRFProtection`
- **`ensureToken(request: Request)`** - Ensures XSRF token is available and returns appropriate headers

## 🔧 TypeScript Support
This package is built with TypeScript and provides full type safety. All API responses are properly typed, and you can extend the types for your specific use cases.
## 🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## ⚠️ Stability Notice
This package is currently experimental. While it's functional and tested, the API may change in future versions. Use with caution in production environments.