Links to the project
📂 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
- Introduction
- Concept
- Frontend
- HTTP Methods & CRUD Operations
- Error Handling
- Integration of Our Own API with Two External APIs
- Authentication via JWT
- Routing & Protected Routes
- Account Creation & Management
- Publish the App on a Production Server
- 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:
- RESTful API
- MVC pattern
- a complete technical specification of the product
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:
- Using all HTTP methods
- 400, 401, 403 error handling
- Integration of our own API with two external APIs
- Auth via JWT
- Project should have clear separation of layers
- Project should have separate backend and frontend
- Usage of database
- User session
- CRUD operations
- Private routes
- Account making
- UML diagram
- Unit tests
- Publish the app on production server
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:
- Guest - Unauthenticated visitor.
- User - Authenticated user. (Initially planned for post commenting and saving blog posts to favourites, but not implemented due to scope constraints.)
- Author - Can create and manage their own posts.
- Administrator - Manages roles and content. Can promote users to authors or admins, edit/delete posts, and manage accounts. (Self-deletion is disabled to avoid losing full administrative control.)
Frontend
During the project I was responsible for the frontend of the application. To accomplish the requirements I went for this technologies:
- React – Library that is my strongest, with a component‑based architecture.
- Vite – Ultra‑fast dev server and build tool, reducing initial setup time and providing an efficient development experience.
- TypeScript – Essential for a large, collaborative project to catch errors at compile time and ensure maintainability.
- TailwindCSS + DaisyUI – Rapid UI development with a utility‑first approach, combined with pre‑built accessible components.
- Motion – Animation library for UI feedback (e.g., notification transitions).
- Axios – Simpler, more expressive HTTP client compared to fetch, with built in support for interceptors and request configuration.
- jwt-decode – For decoding JWTs and extracting user claims without unnecessary backend calls.
- React Router – For structured and predictable client‑side routing.
- TanStack Store – For centralized state management with minimal boilerplate.
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
- How to design clear, reusable API methods for different HTTP verbs.
- Why separating API calls into dedicated modules improves maintainability.
- How to use Axios with typed parameters to reduce runtime errors.
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
- The value of centralizing error handling for consistent UX.
- How to map backend HTTP status codes (400/401/403) to user‑friendly messages.
- Why having a global notification system speeds up debugging and improves feedback.
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:
- Affirmation API – returns short motivational phrases to display on the blog dashboard.
- 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:
- Security – some APIs require API keys, and exposing them in the frontend would be risky. Even though our chosen APIs didn’t require keys, the same pattern works for protected APIs.
- Consistency – by normalizing external API responses in our backend, the frontend always receives data in a predictable format.
- Error Handling – if an external API fails or returns unexpected data, our backend can send a user‑friendly error instead of breaking the UI.
- Future‑proofing – if we ever change the data source, the frontend API contract stays the same.
Backend Flow
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.
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:
serverUrlis 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.- All API‑related logic is abstracted away from components, making components simpler and more focused on rendering UI.
What I Learned
- How to use the backend as a proxy for external data sources.
- Why normalizing external API responses makes frontend integration easier.
- How to handle and recover from failures when third‑party services are unavailable.
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:
- User logs in with email/username and password via
POST /Auth/login. - Backend validates the credentials against the database.
- Backend generates a JWT containing:
- The user’s unique ID.
- The user’s role (
user,author,admin). - Expiration date.
- 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 forlocalStorage) - On every request to a protected endpoint, the frontend sends the token in the
Authorization: Bearer <token>header. - 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
- How to implement a complete stateless authentication flow using JWT from login to role‑based authorization.
- The importance of validating roles on both the frontend (for UX) and backend (for security).
- Why token storage method matters — and how
localStorageis simpler for prototypes, but HTTP‑only cookies are safer for production. - How decoding JWTs on the client can reduce unnecessary API calls and improve performance.
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:
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.
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
- Centralized control – all routes and their permissions are defined in one place.
- Dynamic navigation – the menu updates instantly when a user logs in/out or changes role.
- Easier maintenance – adding a new protected page is as simple as adding one entry to
AppRoutes.
What I Learned
- The value of data‑driven routing — defining routes as configuration makes them flexible and easy to manage.
- Why frontend role checks are for UX, but backend checks are for actual security.
- How to create a routing system that’s scalable and simple to extend.
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:
- View their profile (
GET /Users/get-my-profile). - Update their email or password (
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.
Admin User Management
Admins have full control over all accounts:
- View all users (
GET /Users) - Delete users (
DELETE /Users/{id}) - Update user roles (
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.
Frontend Role Awareness
The React frontend integrates with the Routing & Protected Routes system:
- Admin‑only pages (like “Manage Users”) are hidden from non‑admins.
- Even if someone manually enters an admin URL, the backend still rejects unauthorized requests.
Why This Approach Works Well
- Clear separation between self‑service profile management and admin‑level user management.
- Security at both the frontend (hidden routes) and backend (JWT role verification).
- Scalability — adding new roles or permissions is just a matter of updating role checks.
What I Learned
- How to design a role‑aware user management system that works across the frontend and backend.
- Why server‑side checks are essential even when the frontend hides restricted actions.
- How to keep account APIs modular, so the same endpoints can be reused in different parts of the app.
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:
- Ease of deployment – automatic builds directly from the Git repository.
- Fast global CDN – ensuring the site loads quickly for anyone.
- Zero‑config setup – ideal for a React + Vite stack.
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:
- Straightforward setup – quick deployment without complex configuration.
- Supports .NET backend out of the box.
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:
- The frontend is globally available, fast, and easy to update.
- The backend remains reliably online, with minor cold‑start delays but no functional issues.
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:
- defining requirements
- choosing right technologies
- implementing the frontend
- discuss backend structure and what data should it return
- integrating with APIs
- ensuring secure auth and role-based access control
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. ✌️