begiedz logo begiedz.dev
Case Study

Daily Blog – Full‑stack React & .NET Blogging Platform

Dariusz Begiedza
#frontend#webdev#react#api#deployment#case-study
frontend case-study project describe

📂 GitHub Repository: View Source Code

🔗 Live Demo: View Project on Vercel
(The server has a cold start enabled, give it a minute to warm up.)


Table of Contents

  1. Introduction
  2. Concept
  3. Frontend
  4. HTTP Methods & CRUD Operations
  5. Error Handling
  6. Integration of Our Own API with Two External APIs
  7. Authentication via JWT
  8. Routing & Protected Routes
  9. Account Creation & Management
  10. Publish the App on a Production Server
  11. Conclusion

Introduction

During my 4th semester at university, things started to be more product-oriented. I felt right at home, that’s because I am more goal‑oriented person than theoretical one.

During our labs we were given a project to do. The project required us to build an full-stack application using:

This was for three separate labs, and each lab instructor had different requirements for us to include in the project. We created one project that fulfilled the requirements for all three labs. The summary of them was:

These requirements were beyond what was for the lowest grade, but my classmate and I were ambitous about this. 😅

For the sake of this blog, I will focus mainly on the frontend side of the application.


Concept

The topic of the application was up to us. Only things was – used external APIs should match the use-case of the app. Me and my classmate decided that we will do a blogging platform oriented for the specific topic chosen by the target group – with a simple name Daily Blog.

We made a roles division. The app works based on 4 access levels, deeper the level the more privileges you have:


Frontend

During the project I was responsible for the frontend of the application. To accomplish the requirements I went for this technologies:


HTTP Methods & CRUD Operations

All communication with the backend was handled via Axios. Authentication headers containing the JWT were required for all protected routes.

GET (Read)

GET Method is used for most of the requests, for getting information such as posts, user info, external API.

Here is example of GetPosts method, that fires at page load via useEffect:

export const getPosts = async (pageNumber = 1, pageSize = 7) => {
  try {
    const response = await axios.get(
      `${serverUrl}/Blog/all-posts?pageNumber=${pageNumber}&pageSize=${pageSize}`
    );
    console.log('getPosts response: ', response.data);
    return {
      posts: response.data.posts,
      pagination: response.data.pagination,
    };
  } catch (err) {
    handleApiNotify(err);
    return { posts: [], pagination: null };
  }
};

POST (Create)

POST Method is used for writing data to the database.

export const sendPost = async (postToSend: object) => {
  const token = localStorage.getItem('token');
  try {
    const response = await axios.post(`${serverUrl}/Blog/create-post`, postToSend, {
      headers: { Authorization: `Bearer ${token}` },
    });
    return response;
  } catch (err) {
    handleApiNotify(err);
  }
};

PUT (Update)

PUT is used to update the value. Update of post’s content for example.

export const updatePost = async (id: number, updatedValues: object) => {
  const token = localStorage.getItem('token');
  try {
    const response = await axios.put(`${serverUrl}/Blog/update-post/${id}`, updatedValues, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
    console.log(response.data);
    return response;
  } catch (err) {
    handleApiNotify(err);
  }
};

DELETE

As the name suggests – for deleting things.

export const deletePost = async (id: number) => {
  try {
    const token = localStorage.getItem('token');
    const response = await axios.delete(`${serverUrl}/Blog/delete-post/${id}`, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
    if (response.status !== 200 || response.data.error) {
      throw new Error(response.data.error || 'Failed to delete the post.');
    }
    console.log(response);
    return response;
  } catch (err) {
    handleApiNotify(err);
  }
};

What I Learned


Error Handling

Error handling was implemented through a centralized handleApiNotify function, integrated with a TanStack Store for state management and Motion for animated notifications.

export const handleApiNotify = (resOrErr: AxiosResponse | AxiosError | unknown) => {
  if (axios.isAxiosError(resOrErr)) {
    const err = resOrErr as AxiosError;
    const data = err.response?.data as ApiResponse;

    // image error for creating a post
    const imageError = data?.errors?.Image?.[0];

    setNotification({
      status: err.response?.status || 500,
      message:
        imageError || data?.message || data?.title || err.message || 'Unexpected error occurred.',
      type: 'error',
    });
  } else if ((resOrErr as AxiosResponse)?.status) {
    const res = resOrErr as AxiosResponse;
    const data = res.data as ApiResponse;

    setNotification({
      status: res.status,
      message: data?.message || 'Success.',
      type: 'success',
    });
  }
};
export interface INotification {
  status: number;
  message: string;
  type?: 'success' | 'error' | 'info' | 'warning';
}

notificationStore.ts

import { Store } from '@tanstack/react-store';
import { INotification } from '../types';

interface INotificationState {
  notification: INotification | null;
}

const initialState: INotificationState = {
  notification: null,
};

export const notificationStore = new Store<INotificationState>(initialState);

export const setNotification = (notification: INotification | null) => {
  console.log('Setting notification:', notification);
  notificationStore.setState((prevState) => ({
    ...prevState,
    notification,
  }));
};

What I Learned


Integration of Our Own API with Two External APIs

One of the project requirements was to integrate our application with two external APIs. Instead of calling them directly from the frontend, we had to make it through our own backend API.

This gave us full control over data formatting, error handling, and security before anything reached the client.

We picked two lightweight but interesting APIs:

  1. Affirmation API – returns short motivational phrases to display on the blog dashboard.
  2. NBP Currency Rates API – returns current currency exchange rates from the National Bank of Poland.

Both were purely read-only integrations, so they were safe to expose to end users once filtered through our backend.

We chose them because we wanted to capture the vibe of the early 2000 portal news websites back then, but in modern shape.

Why Go Through Our Own API?

In fact, calling external APIs through our own backend was a requirement from our instructor — we weren’t allowed to call them directly from the frontend.

That said, this approach came with several real‑world advantages that are worth highlighting:

Backend Flow

The backend exposes two endpoints:

The backend wraps these calls in try/catch blocks, logging failures and returning proper HTTP error codes.

That way, the frontend always deals with a consistent API — it doesn’t care whether the data came from our database or from an external service.

Frontend Integration

To make these features reusable and maintainable, I created a dedicated externalApi.ts module:

import axios from 'axios';
import { serverUrl } from '../utils/config';
import { handleApiNotify } from './utils';

// Fetches a random motivational affirmation
export const getAffirmation = async () => {
  try {
    const response = await axios.get(`${serverUrl}/Affirmation/random`);
    return response.data.affirmation;
  } catch (err) {
    handleApiNotify(err);
  }
};

// Fetches currency exchange rates
export const getRates = async () => {
  try {
    const response = await axios.get(`${serverUrl}/Currency/currency-rates`);
    return response.data;
  } catch (err) {
    handleApiNotify(err);
  }
};

Key points:

What I Learned


Authentication via JWT

A secure authentication and authorization flow was a core requirement for the project.

Our instructor specifically required us to use JWT (JSON Web Tokens), which turned out to be an excellent opportunity to learn how modern, stateless authentication works in real applications.

How It Works

The authentication process follows a login → token → authorization flow:

  1. User logs in with email/username and password via POST /Auth/login.
  2. Backend validates the credentials against the database.
  3. Backend generates a JWT containing:
    • The user’s unique ID.
    • The user’s role (userauthoradmin).
    • Expiration date.
  4. Frontend stores the token securely in localStorage. (Better option would be use of HTTP only cookie, but for the sake of the project we decided for localStorage)
  5. On every request to a protected endpoint, the frontend sends the token in the Authorization: Bearer <token>header.
  6. Backend verifies the token, checks the role, and decides if the request is allowed.

Frontend Implementation

I created a dedicated authApi.ts module to handle login, registration, and session management.

Example – Login Request:

import axios from 'axios';
import { serverUrl } from '../utils/config';
import { handleApiNotify } from './utils';

export const loginRequest = async (credentials: { email: string; password: string }) => {
  try {
    const response = await axios.post(`${serverUrl}/Auth/login`, credentials);
    localStorage.setItem('token', response.data.token);
    return response.data;
  } catch (err) {
    handleApiNotify(err);
  }
};

I also used jwt-decode to read the token payload on the client and extract the role without making extra API calls.

What I Learned


Routing & Protected Routes

In Daily Blog, routing isn’t just about navigation — it’s an essential part of the access control system.

I designed the routing layer so it dynamically adjusts based on the logged‑in user’s role, hiding or exposing pages as needed.

Role‑Based Route Configuration

Instead of hard‑coding routes in <Routes> directly, I maintain a single configuration object in AppRoutes.ts.

Each route entry defines:

Example:

{
  name: 'New Post',
  path: '/create',
  pageElement: <Create />,
  role: [ADMIN, AUTHOR],
  icon: <NewPostIcon />,
}

This means only Admins and Authors will see and be able to access the /create route.

Dynamic Route Filtering

At runtime, I call filteredRoutes() with the full route list and the current user from authStore.

This returns only the routes the user is allowed to see:

const user = useStore(authStore, state => state.user);

<Routes>
  {filteredRoutes({ routes: AppRoutes, user }).map((route, index) => (
    <Routekey={index}
      path={route.path}
      element={route.pageElement}
    />
  ))}
</Routes>

If the user isn’t logged in, they only see public routes like //about, or /login.

If they are an Author, they see additional options like New Post and My Posts.

Admins see everything — including the Admin Panel and management screens.

Security in Depth

This frontend filtering improves UX by hiding irrelevant links, but it’s not the only protection.

The backend still re‑validates the user’s JWT and role before allowing any protected API action.

So even if someone tried to manually visit /panel/users without permission, the server would reject the request with a 403: Forbidden.

Benefits of This Setup

What I Learned


Account Creation & Management

User accounts are the foundation of Daily Blog’s authentication and authorization system.

The project requirements included not only account creation but also profile management and role management — all built on top of the JWT authentication layer.

Registration

The process starts with a simple registration form.

When a new user submits their email, username, and password, the backend validates the data and stores it securely in the database.

On success, the user can log in and receive a JWT for authentication.

Although basic registration was required, we designed the system to support role promotion later — for example, turning a regular User into an Author.

Profile Management

Logged‑in users can:

Example – Update Profile API Call:

export const updateUserProfile = async (email: string, password: string) => {
  const token = localStorage.getItem('token');
  try {
    const response = await axios.put(
      `${serverUrl}/Users/update-my-account`,
      { email, password },
      {
        headers: { Authorization: `Bearer ${token}` },
      }
    );
    return response.data;
  } catch (err) {
    handleApiNotify(err);
  }
};

This ensures changes are securely tied to the currently logged‑in user — no one can edit another user’s profile.

Admin User Management

Admins have full control over all accounts:

Example – Change User Role:

export const updateUserRole = async (id: number, role: string) => {
  const token = localStorage.getItem('token');
  try {
    const response = await axios.put(`${serverUrl}/Users/${id}/role`, role, {
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
    });
    return response;
  } catch (err) {
    handleApiNotify(err);
  }
};

Only Admins can perform these actions, and the backend verifies the token and role on every request.

Frontend Role Awareness

The React frontend integrates with the Routing & Protected Routes system:

Why This Approach Works Well

What I Learned


Publish the App on a Production Server

A core requirement for the project was to deploy the application to a live production environment, making it accessible outside the development setup. We treated deployment not just as a final step, but as part of the development workflow.

Frontend Deployment

The frontend was deployed on Vercel, chosen for its:

With Vercel, every push to the master branch triggered an automatic production build.

Backend Deployment

The backend was originally planned to be deployed on Microsoft Azure, as our API was written in .NET and Azure has native integration for .NET applications. However, during testing, we faced issues with release configuration and service setup, which slowed down the deployment process.

To keep the project on schedule, we switched to Render as our hosting platform.

Advantages:

The only downside was the cold start time when the server had been idle for a while — but for our uni project, this was more than acceptable.

Outcome

With this setup:

This deployment allowed our project to be fully accessible online, ready for demonstrations, testing, and further development.


Conclusion

Building Daily Blog was more than just a university assignment – it was a real opportunity to design, build, and deliver a complete product from the ground up.

I worked through every stage of development:

While the scope was ambitious, completing it taught me how to balance functionality, maintainability, and security without sacrificing developer experience (or mental sanity). I learned how important it is to think in a system – where routing, auth, state management, and API integration have to work seamlessly together to deliver consistent experience.

Finally, a big thanks to my classmate – it was a great pleasure to have the opportunity to collaborate closely throughout the entire project. ✌️

← Back to Blog