-->
📂 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.)
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.
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:
During the project I was responsible for the frontend of the application. To accomplish the requirements I went for this technologies:
All communication with the backend was handled via Axios. Authentication headers containing the JWT were required for all protected routes.
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 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 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);
}
};
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);
}
};
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,
}));
};
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:
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.
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:
The backend exposes two endpoints:
GET /Affirmation/random
→ Calls the Affirmation API and returns a string with a motivational phrase.GET /Currency/currency-rates
→ Calls the NBP API and returns a JSON object with exchange rates.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.
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:
serverUrl
is an environment‑specific base URL, so the same code works in development and production.handleApiNotify()
is my centralized notification handler — if an API call fails, the user sees a styled toast with an error message.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.
The authentication process follows a login → token → authorization flow:
POST /Auth/login
.user
, author
, admin
).localStorage
. (Better option would be use of HTTP only cookie, but for the sake of the project we decided for localStorage
)Authorization: Bearer <token>
header.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.
localStorage
is simpler for prototypes, but HTTP‑only cookies are safer for production.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.
Instead of hard‑coding routes in <Routes>
directly, I maintain a single configuration object in AppRoutes.ts
.
Each route entry defines:
name
– for display in the menu (unless hidden).path
– the URL pattern.pageElement
– the React component to render.role
– an array of roles allowed to access this route.icon
– optional icon for menu display.includeInMenu
– whether to show it in the navigation.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.
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.
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
.
AppRoutes
.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.
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.
Logged‑in users can:
GET /Users/get-my-profile
).PUT /Users/update-my-account
).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.
Admins have full control over all accounts:
GET /Users
)DELETE /Users/{id}
)PUT /Users/{id}/role
)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.
The React frontend integrates with the Routing & Protected Routes system:
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.
The frontend was deployed on Vercel, chosen for its:
With Vercel, every push to the master
branch triggered an automatic production build.
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.
With this setup:
This deployment allowed our project to be fully accessible online, ready for demonstrations, testing, and further development.
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. ✌️