MERN stack project with Vite and TypeScript
This is a MERN stack learning project, inspired by this tutorial. It diverges from the original tutorial by using Vite TS instead of Create React App and TypeScript in place of JavaScript. The project aims to demonstrate a practical implementation of the MERN stack, integrating these modern technologies and techniques.
- React: A JavaScript library for building user interfaces.
- TypeScript: A typed superset of JavaScript that compiles to plain JavaScript, enhancing code quality and maintainability.
- Bootstrap: A front-end framework for developing responsive and mobile-first websites.
- Redux and RTK (Redux Toolkit): For efficient state management in React applications.
- Node.js: A JavaScript runtime for building fast and scalable network applications.
- Express.js: A minimal and flexible Node.js web application framework.
- Mongoose: An Object Data Modeling (ODM) library for MongoDB and Node.js.
- MongoDB: A NoSQL database for modern applications with powerful querying and indexing capabilities.
Ensuring type safety across both the frontend and backend of an application is a complex but crucial task. Here's what I've learned about maintaining this consistency:
- Mongoose Schema to TypeScript Types: Inferring TypeScript types from a Mongoose schema and reusing these types in both backend controllers and frontend components involves some intricate steps.
- TypeScript Configuration for Shared Types: As per this StackOverflow solution, including the following configuration in the frontend's
tsconfig.json
allows imports from@backend
to reference the backend folder:
"compilerOptions": {
"paths": {
"@backend/*": ["../backend/types/*"]
},
}
- Advantages of Apollo + GraphQL: A setup like Apollo with GraphQL, which auto-generates types when fetching data to the frontend, presents a more streamlined approach. This allows for a complete separation of frontend and backend code, while still keeping the types in sync.
- Handling
req.body
Types: In Node.js and Express,req.body
often defaults to the typeany
. To establish type safety, especially for client requests and server responses, consider the following approaches:- TypeScript Interface/Type Assertion: For instance, in a controller function like
const { email, password } = req.body
,email
andpassword
are of typeany
. To enforce type safety, define an interface or type and use TypeScript assertion:interface AuthUser { email: string; password: string; } const { email, password } = req.body as AuthUser;
- Runtime Validation with Libraries: TypeScript assertions ensure type safety at compile time. For runtime validation, use libraries like
joi
,express-validator
, orclass-validator
. These tools validate the structure and content of the request body, ensuring that data conforms to the specified types at runtime.
- TypeScript Interface/Type Assertion: For instance, in a controller function like
By implementing these methods, you can enhance the type safety of your backend controllers, ensuring a more robust and error-resistant application.
Turning Mongoose schema definitions into usable TypeScript types or interfaces presents unique challenges, especially when a schema references others. This is evident in complex projects where schemas interlink, like orderSchema
referencing user
and product
in this project.
Key issues include:
- Automatic Property Addition by Mongoose/MongoDB: Mongoose or MongoDB automatically adds properties such as
_id
andcreatedAt
to each document. TypeScript, unaware of these automatic additions, often flags them as errors. For instance, inProfileScreen.tsx
, accessingorder._id
triggers a TypeScript error: "Property_id
does not exist on type 'OrderModelType'." The reason is TypeScript's lack of awareness of these automatically added properties.
To address this:
- Manual Property Addition in TypeScript Types: Extend TypeScript types to include these properties. For example:
type OrderModelType = InferredOrderType & {
_id: Types.ObjectId;
user: UserModelType;
};
This approach, while effective, can become cumbersome in larger projects with multiple schemas and models due to the repetitive nature of manually adding these properties.
Navigating this aspect of Mongoose and TypeScript integration requires careful planning to maintain type safety without excessive manual type extensions, especially in more extensive projects.
The implementation of a private route in React using react-router-dom
is shown below:
const PrivateRoute = () => {
const { userInfo } = useSelector((state: RootState) => state.auth)
if (!userInfo) {
return <Navigate to='/login' replace/>
}
return <Outlet />
}
This example demonstrates a PrivateRoute component that grants access only after user authentication.
- Manipulation through Redux DevTools: The reliance on
userInfo
stored in the Redux store for frontend private routing introduces a potential security vulnerability.- Dispatching Actions via Redux DevTools: An individual could potentially use Redux DevTools in the browser to dispatch an action like
{ type: 'auth/setCredentials', payload: {isAdmin: true} }
. This action would artificially setuserInfo
in the Redux store, granting unauthorized access to private and admin routes. - Dual Role of
auth/setCredentials
: The actionauth/setCredentials
performs two functions:- Storing the
userInfo
object in the Redux store. - Storing the same
userInfo
in localStorage.
- Storing the
- Accessing Protected Routes: Due to this, manipulating
userInfo
through Redux DevTools can potentially provide unwarranted access to frontend private and admin routes.
- Dispatching Actions via Redux DevTools: An individual could potentially use Redux DevTools in the browser to dispatch an action like
This highlights a critical security issue in using client-side state management for access control. A robust backend authentication and authorization check should always accompany such measures to ensure secure access control.