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.jsonallows imports from@backendto 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.bodyTypes: In Node.js and Express,req.bodyoften 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,emailandpasswordare 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
_idandcreatedAtto each document. TypeScript, unaware of these automatic additions, often flags them as errors. For instance, inProfileScreen.tsx, accessingorder._idtriggers a TypeScript error: "Property_iddoes 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
userInfostored 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 setuserInfoin the Redux store, granting unauthorized access to private and admin routes. - Dual Role of
auth/setCredentials: The actionauth/setCredentialsperforms two functions:- Storing the
userInfoobject in the Redux store. - Storing the same
userInfoin localStorage.
- Storing the
- Accessing Protected Routes: Due to this, manipulating
userInfothrough 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.