frontend Tutorial

Building an Expense Tracker Frontend - Part 2: Auth Integration

In this part, we will connect our frontend to the Scala backend we built earlier. We'll implement the Login functionality using TanStack Query mutations and secure our API calls with Axios Interceptors.

Setting up Axios

We need a centralized way to make API calls. We'll configure an Axios instance that automatically attaches our JWT token to every request. This is much cleaner than manually adding the header to every single API call.

Create src/api/axios.ts:

// src/api/axios.ts
import axios from 'axios';

const api = axios.create({
  baseURL: 'http://localhost:9000/api',
});

// Add a request interceptor
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

export default api;

Why Interceptors? Interceptors act like middleware for your HTTP requests. By checking for the token in localStorage and adding it to the Authorization header, we ensure that all our requests are authenticated by default.

Auth API Service

Now, let's create a service file to define our authentication API calls. This keeps our component code clean and focused on UI logic.

// src/api/auth.ts
import api from './axios';

export const login = async (email: string, password: string) => {
  const response = await api.post('/auth/login', { email, password });
  return response.data;
};

// We can add register and logout here later

Login Component with TanStack Query

We'll create a LoginForm component. Instead of manually managing isLoading and error states with useState, we'll use TanStack Query's useMutation.

useMutation is perfect for creating, updating, or deleting data (POST, PUT, DELETE requests).

// src/components/LoginForm.tsx
import React, { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { login } from '../api/auth';
import { useNavigate } from 'react-router-dom';

const LoginForm: React.FC = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const navigate = useNavigate();

  const mutation = useMutation({
    mutationFn: () => login(email, password),
    onSuccess: (data) => {
      console.log('Login successful:', data);
      // Save token to localStorage
      localStorage.setItem('token', data.token);
      // Redirect to dashboard
      navigate('/dashboard');
    },
    onError: (error) => {
      console.error('Login failed:', error);
      alert('Invalid credentials');
    },
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    mutation.mutate();
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <div>
        <label className="block text-sm font-medium text-gray-700">Email</label>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
          required
        />
      </div>
      <div>
        <label className="block text-sm font-medium text-gray-700">Password</label>
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
          required
        />
      </div>
      <button
        type="submit"
        disabled={mutation.isPending}
        className="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700 disabled:opacity-50"
      >
        {mutation.isPending ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
};

export default LoginForm;

Login Page

Finally, we wrap the form in a page layout.

// src/pages/LoginPage.tsx
import React from 'react';
import LoginForm from '../components/LoginForm';

const LoginPage: React.FC = () => {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full p-6 bg-white rounded-lg shadow-md">
        <h2 className="text-center text-3xl font-bold mb-8 text-gray-900">Sign in</h2>
        <LoginForm />
      </div>
    </div>
  );
};

export default LoginPage;

Next Steps

In the next part, we will build the Dashboard and display the list of expenses by fetching data from our protected API endpoints.

Continue to Part 3: Dashboard & Expenses List