@mngl/sanctum-spa-client
v0.4.0
Published
Lightweight TypeScript client for Laravel Sanctum SPA XSRF protection using Axios.
Readme
@mngl/sanctum-spa-client
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
npm install @mngl/sanctum-spa-client🛠️ Setup
Project Structure
app/
├── src/
│ └── services/
│ └── api/
│ ├── backend/
│ │ └── backendApi.server.ts
│ ├── frontend/
│ │ └── clientApi.client.ts
│ └── queries/
│ └── loginUser.tsBackend API Configuration (Server-side)
backendApi.server.ts
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
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
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
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
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
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: "[email protected]",
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: "[email protected]",
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
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:
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 objectrequestAndUnwrap<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 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.
