DEV Community

Nadim Chowdhury
Nadim Chowdhury

Posted on

Developing a Scalable Car Rental App: A Full-Stack Guide with Next.js and NestJS

To develop a Car Rental System using Next.js, NestJS, Tailwind CSS, and PostgreSQL, you can follow a structured approach to create both the frontend and backend, with proper state management, UI design, and database interaction. Here's a high-level plan with suggested technologies and architecture.

Architecture Overview

1. Front End (Next.js with Tailwind CSS):

The frontend will allow users to:

  • Search for available cars.
  • Filter cars by price, brand, and availability.
  • View detailed car listings with specifications and price comparison.
  • Book rentals and manage their bookings.

You can leverage Next.js to build a fast, server-rendered React application. Key features like routing, data fetching, and SSR (Server Side Rendering) can be used to ensure smooth and efficient page loads. You can also integrate Tailwind CSS to manage the styling efficiently.

2. Back End (NestJS with PostgreSQL):

The backend will manage:

  • Car listings (CRUD operations for admins).
  • User profiles, bookings, and rental history.
  • Payment gateways integration for rental fees.
  • Admin interface to track rental operations, available cars, and bookings.

NestJS will provide a scalable backend API with features like authentication (JWT), ORM (Object Relational Mapping) via TypeORM to interact with PostgreSQL, and other modular services like notifications.

3. Mobile App:

For mobile, you can use React Native for a cross-platform mobile experience where users can:

  • Browse and book rentals.
  • Track their rental history.
  • Receive push notifications for updates.

Key Technologies

  1. Frontend:

    • Next.js: For building a scalable and performant frontend.
    • Tailwind CSS: For styling and responsive design.
    • Axios (or fetch): For interacting with the NestJS backend.
  2. Backend:

    • NestJS: For building the API, managing routes, authentication, and business logic.
    • PostgreSQL: For storing car listings, user profiles, and bookings.
    • TypeORM: For seamless interaction between NestJS and PostgreSQL.
  3. Mobile App:

    • React Native: For building the cross-platform mobile app.

Step-by-Step Breakdown

Step 1: Set up the Frontend (Next.js)

  1. Install Next.js with Tailwind CSS:
   npx create-next-app@latest car-rental-frontend
   cd car-rental-frontend
   npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

Configure Tailwind CSS in your Next.js project by editing tailwind.config.js and including Tailwind directives in your global CSS file.

  1. Create Pages:

    • /cars: List available cars with filters for price, brand, and availability.
    • /car/[id]: Dynamic route to display individual car details.
    • /bookings: User dashboard to manage bookings.
  2. Use Next.js Features:

    • Use API routes to communicate with the backend.
    • Implement SSR/SSG for SEO optimization and faster loading.

Step 2: Set up the Backend (NestJS with PostgreSQL)

  1. Create NestJS Project:
   nest new car-rental-backend
Enter fullscreen mode Exit fullscreen mode
  1. Install Required Packages:

    • TypeORM and PostgreSQL for database:
     npm install @nestjs/typeorm typeorm pg
    
  • JWT Authentication:

     npm install @nestjs/jwt passport-jwt
    
  • Payment integration (e.g., Stripe or PayPal).

  1. Set up Modules:

    • AuthModule: Handle user registration, login, JWT authentication.
    • CarsModule: Manage car listings (CRUD operations for admin).
    • BookingModule: Manage bookings and rental logic.
    • PaymentModule: Manage payment transactions.
  2. Set up Database:
    Define your entities (Car, User, Booking) with TypeORM and handle migrations.

   @Entity()
   export class Car {
       @PrimaryGeneratedColumn()
       id: number;

       @Column()
       name: string;

       @Column()
       brand: string;

       @Column('decimal')
       pricePerDay: number;

       @Column()
       availability: boolean;
   }
Enter fullscreen mode Exit fullscreen mode
  1. Create API Endpoints:
    • POST /auth/login: Handle user authentication.
    • GET /cars: Fetch available cars.
    • POST /bookings: Handle bookings.
    • GET /bookings/:id: Get booking details.

Step 3: Mobile App with React Native

  1. Set up React Native:
   npx react-native init CarRentalApp
Enter fullscreen mode Exit fullscreen mode

Integrate the API from the backend using Axios or fetch.

  1. Create Screens:

    • Home: Browse available cars.
    • Car Details: View car details and proceed with booking.
    • Bookings: View past and current bookings.
  2. Notifications:
    Use Firebase Cloud Messaging (FCM) or another service to notify users about updates on their rentals.

Deployment Considerations

  • Frontend: Deploy on platforms like Vercel (ideal for Next.js apps).
  • Backend: Host the backend API on Heroku, AWS, or any cloud provider.
  • Database: Use AWS RDS or another cloud-hosted PostgreSQL service.

This modular architecture will help you scale the application in the future while keeping the code maintainable. Let me know if you need help with any specific step, such as setting up authentication or database relations!

For a Next.js frontend of your Car Rental System, using Tailwind CSS for styling and organizing the project with best practices, here’s a proper file and folder structure:

Project Structure

car-rental-frontend/
├── public/
│   ├── images/
│   └── favicon.ico
├── src/
│   ├── components/
│   │   ├── CarCard.tsx
│   │   ├── CarFilter.tsx
│   │   ├── Navbar.tsx
│   │   └── Footer.tsx
│   ├── pages/
│   │   ├── api/
│   │   │   └── auth.ts
│   │   ├── cars/
│   │   │   ├── index.tsx
│   │   │   └── [id].tsx
│   │   ├── bookings/
│   │   │   ├── index.tsx
│   │   └── index.tsx
│   ├── styles/
│   │   ├── globals.css
│   │   └── tailwind.css
│   ├── utils/
│   │   ├── axios.ts
│   │   └── helpers.ts
│   ├── hooks/
│   │   └── useAuth.ts
│   ├── contexts/
│   │   └── AuthContext.tsx
│   ├── services/
│   │   └── carService.ts
│   └── layouts/
│       └── MainLayout.tsx
├── tailwind.config.js
├── postcss.config.js
├── next.config.js
├── package.json
└── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

Folder Breakdown

1. public/

This folder holds all static assets that need to be served directly, such as images, logos, and the favicon.

  • images/: Store your static images (car photos, logos, etc.).
  • favicon.ico: The website's favicon.

2. src/ (Source Directory)

The main application code lives here, structured for scalability and modularity.

2.1 components/

Reusable UI components are placed here. Each component is self-contained, making the code modular and easy to maintain.

  • CarCard.tsx: Displays a car in the list with details like name, price, and image.
  • CarFilter.tsx: A filter component for searching by price, availability, or brand.
  • Navbar.tsx: The navigation bar used across the website.
  • Footer.tsx: The footer component.
2.2 pages/ (Next.js Routing)

Next.js uses the pages/ directory for routing. Each file inside this folder becomes a route.

  • index.tsx: The home page, showing car listings and filters.
  • cars/:
    • index.tsx: The list of all available cars with filtering options.
    • [id].tsx: Dynamic route to display individual car details (e.g., /cars/123 for a car with id 123).
  • bookings/:
    • index.tsx: The page where users can view their bookings and manage them.

Next.js automatically handles routing based on the folder and file structure. Dynamic routes are defined by wrapping parts of filenames with square brackets ([id].tsx for individual car pages).

2.3 styles/

For Tailwind CSS and any global styles.

  • globals.css: General styles or global overrides.
  • tailwind.css: Tailwind CSS imports and configuration.
2.4 utils/

Helper functions and utility code.

  • axios.ts: Configure your Axios instance for making API calls to the NestJS backend.
  • helpers.ts: General utility functions (e.g., format prices, date handling).
2.5 hooks/

Custom React hooks to reuse logic.

  • useAuth.ts: A custom hook for managing authentication state (login, logout, JWT token handling).
2.6 contexts/

Context API files for managing global state.

  • AuthContext.tsx: Context provider to manage user authentication globally (provides user data, login, and logout functionality).
2.7 services/

Service files for API interactions.

  • carService.ts: Functions for fetching car data, car availability, booking a car, etc.
2.8 layouts/

Layout components for structuring your pages. Layouts are used for consistent page structures like the header, footer, and navigation.

  • MainLayout.tsx: A common layout used by most pages. It can include components like Navbar, Footer, and have a children prop for rendering specific page content.

Root-Level Files

  • tailwind.config.js: Configuration for Tailwind CSS.
  • postcss.config.js: PostCSS configuration (for transforming Tailwind CSS).
  • next.config.js: Next.js configuration file, where custom settings can be defined.
  • package.json: Holds project dependencies and scripts.
  • tsconfig.json: Configuration file for TypeScript.

Example Code Snippets

components/CarCard.tsx

import React from 'react';

interface CarCardProps {
  id: number;
  name: string;
  brand: string;
  pricePerDay: number;
  imageUrl: string;
}

const CarCard: React.FC<CarCardProps> = ({ id, name, brand, pricePerDay, imageUrl }) => {
  return (
    <div className="bg-white shadow-md rounded-lg overflow-hidden">
      <img src={imageUrl} alt={name} className="w-full h-48 object-cover" />
      <div className="p-4">
        <h3 className="text-lg font-bold">{name}</h3>
        <p className="text-sm text-gray-600">{brand}</p>
        <p className="text-md font-semibold text-indigo-600">${pricePerDay} / day</p>
      </div>
    </div>
  );
};

export default CarCard;
Enter fullscreen mode Exit fullscreen mode

layouts/MainLayout.tsx

import React from 'react';
import Navbar from '../components/Navbar';
import Footer from '../components/Footer';

const MainLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  return (
    <div className="min-h-screen flex flex-col">
      <Navbar />
      <main className="flex-grow">{children}</main>
      <Footer />
    </div>
  );
};

export default MainLayout;
Enter fullscreen mode Exit fullscreen mode

pages/cars/[id].tsx

import { GetServerSideProps } from 'next';
import MainLayout from '../../layouts/MainLayout';
import { fetchCarById } from '../../services/carService';

interface CarPageProps {
  car: {
    id: number;
    name: string;
    brand: string;
    pricePerDay: number;
    description: string;
  };
}

const CarPage: React.FC<CarPageProps> = ({ car }) => {
  return (
    <MainLayout>
      <div className="container mx-auto p-4">
        <h1 className="text-2xl font-bold">{car.name}</h1>
        <p className="text-md">{car.brand}</p>
        <p className="text-xl text-indigo-600">${car.pricePerDay} / day</p>
        <p className="mt-4">{car.description}</p>
      </div>
    </MainLayout>
  );
};

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { id } = context.params!;
  const car = await fetchCarById(id as string);
  return { props: { car } };
};

export default CarPage;
Enter fullscreen mode Exit fullscreen mode

This structure will allow for an organized and scalable frontend. If you need additional details for specific functionality or setup (like authentication, booking, or API integration), feel free to ask!

For the Car Rental System backend, using NestJS, PostgreSQL, and TypeORM, you'll want to organize your project in a modular, scalable way. Below is a complete file and folder structure for your NestJS backend, along with brief descriptions for each part.

Backend File and Folder Structure

car-rental-backend/
├── src/
│   ├── auth/
│   │   ├── dto/
│   │   │   ├── login.dto.ts
│   │   │   ├── register.dto.ts
│   │   ├── auth.controller.ts
│   │   ├── auth.module.ts
│   │   ├── auth.service.ts
│   │   ├── jwt.strategy.ts
│   │   └── local.strategy.ts
│   ├── cars/
│   │   ├── dto/
│   │   │   ├── create-car.dto.ts
│   │   │   ├── update-car.dto.ts
│   │   ├── entities/
│   │   │   └── car.entity.ts
│   │   ├── cars.controller.ts
│   │   ├── cars.module.ts
│   │   ├── cars.service.ts
│   ├── bookings/
│   │   ├── dto/
│   │   │   ├── create-booking.dto.ts
│   │   │   ├── update-booking.dto.ts
│   │   ├── entities/
│   │   │   └── booking.entity.ts
│   │   ├── bookings.controller.ts
│   │   ├── bookings.module.ts
│   │   ├── bookings.service.ts
│   ├── users/
│   │   ├── dto/
│   │   │   └── create-user.dto.ts
│   │   ├── entities/
│   │   │   └── user.entity.ts
│   │   ├── users.controller.ts
│   │   ├── users.module.ts
│   │   ├── users.service.ts
│   ├── payments/
│   │   ├── dto/
│   │   │   └── create-payment.dto.ts
│   │   ├── payments.controller.ts
│   │   ├── payments.module.ts
│   │   ├── payments.service.ts
│   ├── common/
│   │   ├── decorators/
│   │   │   └── roles.decorator.ts
│   │   ├── guards/
│   │   │   └── jwt-auth.guard.ts
│   │   ├── interceptors/
│   │   │   └── logging.interceptor.ts
│   │   ├── filters/
│   │   │   └── http-exception.filter.ts
│   │   └── constants.ts
│   ├── database/
│   │   ├── entities/
│   │   │   └── base.entity.ts
│   │   ├── database.module.ts
│   │   └── database.service.ts
│   ├── app.module.ts
│   ├── main.ts
├── config/
│   ├── jwt.config.ts
│   ├── typeorm.config.ts
│   └── app.config.ts
├── migrations/
│   ├── 1682570845798-CreateCarEntity.ts
│   └── 1682570845799-CreateBookingEntity.ts
├── .env
├── .gitignore
├── nest-cli.json
├── tsconfig.json
├── package.json
└── README.md
Enter fullscreen mode Exit fullscreen mode

Folder Breakdown

1. src/ (Source Code Directory)

This is the main directory where all the application logic lives.

1.1 auth/

Handles the authentication process, including user login, registration, JWT token management, and strategies.

  • auth.controller.ts: Handles HTTP routes related to authentication (e.g., /login, /register).
  • auth.module.ts: Encapsulates the authentication-related services and controllers.
  • auth.service.ts: Contains the business logic for user authentication and token management.
  • jwt.strategy.ts: JWT-based strategy for protecting routes.
  • local.strategy.ts: For handling local username/password authentication.
  • dto/: Data Transfer Objects for validating and typing data during registration and login processes.
    • login.dto.ts: DTO for login requests.
    • register.dto.ts: DTO for registration requests.
1.2 cars/

Handles car-related operations, such as CRUD operations on car listings.

  • cars.controller.ts: API routes for car listings (e.g., /cars, /cars/:id).
  • cars.module.ts: Encapsulates the car-related services and controllers.
  • cars.service.ts: Contains the logic for managing car data.
  • entities/: Defines the data models for the cars.
    • car.entity.ts: The TypeORM entity for the Car model.
1.3 bookings/

Manages the booking process for renting cars.

  • bookings.controller.ts: API routes for booking operations (e.g., /bookings, /bookings/:id).
  • bookings.module.ts: Encapsulates the booking-related services and controllers.
  • bookings.service.ts: Business logic for managing bookings.
  • entities/: TypeORM entity for the bookings.
    • booking.entity.ts: The Booking entity representing the booking data in the database.
1.4 users/

Manages user profiles and related operations.

  • users.controller.ts: API routes for managing user profiles (e.g., /users, /users/:id).
  • users.module.ts: Encapsulates user services and controllers.
  • users.service.ts: Business logic for managing users.
  • entities/: Contains the user-related entities.
    • user.entity.ts: TypeORM entity for the User model.
1.5 payments/

Handles payment processing and integration with third-party payment gateways like Stripe or PayPal.

  • payments.controller.ts: API routes for payments.
  • payments.module.ts: Encapsulates payment services and controllers.
  • payments.service.ts: Business logic for payment processing.
1.6 common/

Contains reusable modules, decorators, guards, interceptors, and filters that can be used across the app.

  • decorators/: Custom decorators like @Roles().
    • roles.decorator.ts: Handles role-based access control.
  • guards/: Guards like JWTGuard to protect routes.
    • jwt-auth.guard.ts: Guard to verify JWT token validity.
  • interceptors/: HTTP interceptors.
    • logging.interceptor.ts: Example of logging interceptor for requests.
  • filters/: Exception filters.
    • http-exception.filter.ts: Handles global HTTP exceptions.
1.7 database/

Manages database connection and entity definitions.

  • base.entity.ts: Base entity that all other entities can extend (e.g., with common fields like createdAt, updatedAt).
  • database.module.ts: Encapsulates the database connection logic.
  • database.service.ts: Service for database management and connection pooling.
1.8 app.module.ts

The root module of your NestJS app that imports all other modules (auth, cars, bookings, etc.).

1.9 main.ts

The entry point of your NestJS application. This file bootstraps the application.

2. config/

This folder holds configuration files for different parts of the application, such as JWT, TypeORM, and global app configuration.

  • jwt.config.ts: JWT configuration (secret, expiration).
  • typeorm.config.ts: Database configuration for PostgreSQL, includes database connection settings.
  • app.config.ts: General application configurations, such as CORS, global prefix, etc.

3. migrations/

This folder contains database migration files generated by TypeORM.

  • 1682570845798-CreateCarEntity.ts: A migration to create the Car entity.
  • 1682570845799-CreateBookingEntity.ts: A migration to create the Booking entity.

Root-Level Files

  • .env: Environment variables (e.g., database connection, JWT secret, API keys).
  • .gitignore: Specifies files to ignore in version control (e.g., node_modules/, .env).
  • nest-cli.json: NestJS CLI configuration file.
  • tsconfig.json: TypeScript configuration file.
  • package.json: Lists project dependencies and scripts.
  • README.md: A readme file for documenting the project setup, purpose, and usage.

Example Code Snippets

src/cars/entities/car.entity.ts

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
export class Car {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()


  name: string;

  @Column()
  brand: string;

  @Column('decimal')
  pricePerDay: number;

  @Column()
  availability: boolean;
}
Enter fullscreen mode Exit fullscreen mode

src/bookings/entities/booking.entity.ts

import { Entity, PrimaryGeneratedColumn, ManyToOne, Column } from 'typeorm';
import { Car } from '../../cars/entities/car.entity';
import { User } from '../../users/entities/user.entity';

@Entity()
export class Booking {
  @PrimaryGeneratedColumn()
  id: number;

  @ManyToOne(() => Car)
  car: Car;

  @ManyToOne(() => User)
  user: User;

  @Column('date')
  rentalDate: string;

  @Column('date')
  returnDate: string;

  @Column('decimal')
  totalAmount: number;
}
Enter fullscreen mode Exit fullscreen mode

src/auth/jwt.strategy.ts

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { JwtPayload } from './jwt-payload.interface';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_SECRET,
    });
  }

  async validate(payload: JwtPayload) {
    return { userId: payload.sub, username: payload.username };
  }
}
Enter fullscreen mode Exit fullscreen mode

This structure will help you keep your backend clean, modular, and scalable, allowing each module (e.g., cars, bookings, payments) to handle its own logic independently. Let me know if you need further details or help setting up specific parts!

Here is the full code for the specified folder structure, including the components, pages, and API route for a basic Car Rental System using Next.js and Tailwind CSS. This setup includes components like CarCard, CarFilter, Navbar, and Footer, as well as pages for car listings, booking management, and authentication API routes.

Project Structure

car-rental-frontend/
├── public/
│   ├── images/
│   └── favicon.ico
├── src/
│   ├── components/
│   │   ├── CarCard.tsx
│   │   ├── CarFilter.tsx
│   │   ├── Navbar.tsx
│   │   └── Footer.tsx
│   ├── pages/
│   │   ├── api/
│   │   │   └── auth.ts
│   │   ├── cars/
│   │   │   ├── index.tsx
│   │   │   └── [id].tsx
│   │   ├── bookings/
│   │   │   └── index.tsx
│   │   └── index.tsx
Enter fullscreen mode Exit fullscreen mode

1. components/CarCard.tsx

This component represents a single car card in a list, displaying the car's details.

import React from 'react';

interface CarCardProps {
  id: number;
  name: string;
  brand: string;
  pricePerDay: number;
  imageUrl: string;
}

const CarCard: React.FC<CarCardProps> = ({ id, name, brand, pricePerDay, imageUrl }) => {
  return (
    <div className="bg-white shadow-md rounded-lg overflow-hidden">
      <img src={imageUrl} alt={name} className="w-full h-48 object-cover" />
      <div className="p-4">
        <h3 className="text-lg font-bold">{name}</h3>
        <p className="text-sm text-gray-600">{brand}</p>
        <p className="text-md font-semibold text-indigo-600">${pricePerDay} / day</p>
      </div>
    </div>
  );
};

export default CarCard;
Enter fullscreen mode Exit fullscreen mode

2. components/CarFilter.tsx

A filter component to allow users to search for cars by brand, price, and availability.

import React, { useState } from 'react';

interface CarFilterProps {
  onFilterChange: (filters: { brand: string; price: number }) => void;
}

const CarFilter: React.FC<CarFilterProps> = ({ onFilterChange }) => {
  const [brand, setBrand] = useState('');
  const [price, setPrice] = useState(100);

  const handleFilterChange = () => {
    onFilterChange({ brand, price });
  };

  return (
    <div className="flex space-x-4 mb-4">
      <input
        type="text"
        placeholder="Brand"
        value={brand}
        onChange={(e) => setBrand(e.target.value)}
        className="border p-2 rounded"
      />
      <input
        type="number"
        placeholder="Max Price"
        value={price}
        onChange={(e) => setPrice(Number(e.target.value))}
        className="border p-2 rounded"
      />
      <button
        onClick={handleFilterChange}
        className="bg-indigo-600 text-white p-2 rounded"
      >
        Apply Filters
      </button>
    </div>
  );
};

export default CarFilter;
Enter fullscreen mode Exit fullscreen mode

3. components/Navbar.tsx

A simple navigation bar component.

import React from 'react';
import Link from 'next/link';

const Navbar: React.FC = () => {
  return (
    <nav className="bg-gray-800 p-4">
      <div className="container mx-auto flex justify-between">
        <Link href="/">
          <a className="text-white font-bold text-lg">Car Rental</a>
        </Link>
        <div>
          <Link href="/cars">
            <a className="text-white mr-4">Cars</a>
          </Link>
          <Link href="/bookings">
            <a className="text-white">Bookings</a>
          </Link>
        </div>
      </div>
    </nav>
  );
};

export default Navbar;
Enter fullscreen mode Exit fullscreen mode

4. components/Footer.tsx

A basic footer component.

import React from 'react';

const Footer: React.FC = () => {
  return (
    <footer className="bg-gray-800 p-4 mt-8 text-center text-white">
      <p>© 2024 Car Rental System. All rights reserved.</p>
    </footer>
  );
};

export default Footer;
Enter fullscreen mode Exit fullscreen mode

5. pages/api/auth.ts

An API route that handles user authentication (simplified).

import type { NextApiRequest, NextApiResponse } from 'next';

export default (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === 'POST') {
    const { username, password } = req.body;

    if (username === 'user' && password === 'password') {
      return res.status(200).json({ token: 'fake-jwt-token' });
    }

    return res.status(401).json({ message: 'Invalid credentials' });
  }

  return res.status(405).json({ message: 'Method not allowed' });
};
Enter fullscreen mode Exit fullscreen mode

6. pages/cars/index.tsx

The car listing page with filtering capabilities.

import React, { useState } from 'react';
import CarCard from '../../components/CarCard';
import CarFilter from '../../components/CarFilter';
import MainLayout from '../../layouts/MainLayout';

const carsData = [
  { id: 1, name: 'Tesla Model S', brand: 'Tesla', pricePerDay: 100, imageUrl: '/images/tesla.jpg' },
  { id: 2, name: 'BMW i8', brand: 'BMW', pricePerDay: 150, imageUrl: '/images/bmw.jpg' },
  { id: 3, name: 'Audi A8', brand: 'Audi', pricePerDay: 120, imageUrl: '/images/audi.jpg' },
];

const CarsPage: React.FC = () => {
  const [filteredCars, setFilteredCars] = useState(carsData);

  const handleFilterChange = (filters: { brand: string; price: number }) => {
    const filtered = carsData.filter(
      (car) =>
        car.brand.toLowerCase().includes(filters.brand.toLowerCase()) &&
        car.pricePerDay <= filters.price
    );
    setFilteredCars(filtered);
  };

  return (
    <MainLayout>
      <div className="container mx-auto p-4">
        <h1 className="text-2xl font-bold mb-4">Available Cars</h1>
        <CarFilter onFilterChange={handleFilterChange} />
        <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
          {filteredCars.map((car) => (
            <CarCard key={car.id} {...car} />
          ))}
        </div>
      </div>
    </MainLayout>
  );
};

export default CarsPage;
Enter fullscreen mode Exit fullscreen mode

7. pages/cars/[id].tsx

The car details page (dynamic route for each car).

import React from 'react';
import { GetServerSideProps } from 'next';
import MainLayout from '../../layouts/MainLayout';

interface CarDetailsProps {
  car: {
    id: number;
    name: string;
    brand: string;
    pricePerDay: number;
    imageUrl: string;
    description: string;
  };
}

const CarDetailsPage: React.FC<CarDetailsProps> = ({ car }) => {
  return (
    <MainLayout>
      <div className="container mx-auto p-4">
        <img src={car.imageUrl} alt={car.name} className="w-full h-64 object-cover rounded-md mb-4" />
        <h1 className="text-3xl font-bold mb-2">{car.name}</h1>
        <p className="text-lg mb-2">Brand: {car.brand}</p>
        <p className="text-lg mb-2 text-indigo-600">Price: ${car.pricePerDay} / day</p>
        <p>{car.description}</p>
      </div>
    </MainLayout>
  );
};

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { id } = context.params!;

  const car = {
    id,
    name: `Car ${id}`,
    brand: 'Brand',
    pricePerDay: 100,
    imageUrl: '/images/car.jpg',
    description: 'A detailed description of the car.',
  };

  return {
    props: {
      car,
    },
  };
};

export default CarDetailsPage;
Enter fullscreen mode Exit fullscreen mode

8. pages/bookings/index.tsx

The bookings page where users can view their past and upcoming bookings.

import React from 'react';
import MainLayout from '../../layouts/MainLayout';

const bookingsData = [
  { id: 1, car: 'Tesla Model S', date: '2024-09-15' },


  { id: 2, car: 'BMW i8', date: '2024-09-20' },
];

const BookingsPage: React.FC = () => {
  return (
    <MainLayout>
      <div className="container mx-auto p-4">
        <h1 className="text-2xl font-bold mb-4">Your Bookings</h1>
        <ul className="list-disc pl-5">
          {bookingsData.map((booking) => (
            <li key={booking.id} className="mb-2">
              <span className="font-bold">{booking.car}</span> - Booking Date: {booking.date}
            </li>
          ))}
        </ul>
      </div>
    </MainLayout>
  );
};

export default BookingsPage;
Enter fullscreen mode Exit fullscreen mode

9. pages/index.tsx

The homepage.

import React from 'react';
import Link from 'next/link';
import MainLayout from '../layouts/MainLayout';

const HomePage: React.FC = () => {
  return (
    <MainLayout>
      <div className="container mx-auto p-4 text-center">
        <h1 className="text-4xl font-bold mb-6">Welcome to Car Rental</h1>
        <p className="text-lg mb-4">Browse through our wide range of cars available for rental.</p>
        <Link href="/cars">
          <a className="bg-indigo-600 text-white px-4 py-2 rounded">View Cars</a>
        </Link>
      </div>
    </MainLayout>
  );
};

export default HomePage;
Enter fullscreen mode Exit fullscreen mode

This setup provides a well-structured Next.js project with car listings, filtering, bookings, and car details. You can easily expand it by adding more functionality such as authentication, booking management, or integration with a backend API. Let me know if you need help with further customizations!

Here's the full code for the additional parts of your project, including styles, utils, hooks, contexts, services, and layouts for your Car Rental System built using Next.js, Tailwind CSS, and TypeScript.

1. styles/globals.css

This file contains your global CSS and Tailwind configuration.

@tailwind base;
@tailwind components;
@tailwind utilities;

html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}

* {
  box-sizing: border-box;
}
Enter fullscreen mode Exit fullscreen mode

2. styles/tailwind.css

This is your entry point for Tailwind-specific styles. It enables the Tailwind CSS classes to be used across your app.

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

3. utils/axios.ts

Here, you create an Axios instance that can be reused throughout your application for API requests. This is where you can configure base URLs, headers, and other options.

import axios from 'axios';

const axiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000/api',
  headers: {
    'Content-Type': 'application/json',
  },
});

export default axiosInstance;
Enter fullscreen mode Exit fullscreen mode

4. utils/helpers.ts

Utility functions that can be reused across the app.

// Utility to format currency values
export const formatPrice = (price: number): string => {
  return `$${price.toFixed(2)}`;
};

// Utility to format dates
export const formatDate = (date: string): string => {
  const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' };
  return new Date(date).toLocaleDateString(undefined, options);
};
Enter fullscreen mode Exit fullscreen mode

5. hooks/useAuth.ts

This custom hook helps manage the authentication state in your application. It checks if the user is logged in, handles login/logout, and manages tokens.

import { useContext } from 'react';
import { AuthContext } from '../contexts/AuthContext';

export const useAuth = () => {
  const { user, login, logout } = useContext(AuthContext);

  return { user, login, logout };
};
Enter fullscreen mode Exit fullscreen mode

6. contexts/AuthContext.tsx

The authentication context manages global state for the user's authentication status, providing login, logout, and user across the app.

import React, { createContext, useState, useEffect } from 'react';
import axios from '../utils/axios';

interface AuthContextProps {
  user: any;
  login: (username: string, password: string) => Promise<void>;
  logout: () => void;
}

export const AuthContext = createContext<AuthContextProps>({
  user: null,
  login: async () => {},
  logout: () => {},
});

export const AuthProvider: React.FC = ({ children }) => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const storedUser = localStorage.getItem('user');
    if (storedUser) {
      setUser(JSON.parse(storedUser));
    }
  }, []);

  const login = async (username: string, password: string) => {
    const response = await axios.post('/auth', { username, password });
    setUser(response.data.user);
    localStorage.setItem('user', JSON.stringify(response.data.user));
  };

  const logout = () => {
    setUser(null);
    localStorage.removeItem('user');
  };

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

7. services/carService.ts

This service interacts with the backend API to fetch car data, manage bookings, etc.

import axios from '../utils/axios';

// Fetch all cars
export const fetchCars = async () => {
  const response = await axios.get('/cars');
  return response.data;
};

// Fetch a single car by ID
export const fetchCarById = async (id: string | number) => {
  const response = await axios.get(`/cars/${id}`);
  return response.data;
};

// Book a car
export const bookCar = async (carId: string | number, userId: string | number, rentalDate: string, returnDate: string) => {
  const response = await axios.post(`/bookings`, { carId, userId, rentalDate, returnDate });
  return response.data;
};
Enter fullscreen mode Exit fullscreen mode

8. layouts/MainLayout.tsx

This layout wraps all your pages to provide a consistent structure across your app. It includes the Navbar, Footer, and slots for the main content.

import React from 'react';
import Navbar from '../components/Navbar';
import Footer from '../components/Footer';

const MainLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  return (
    <div className="min-h-screen flex flex-col">
      <Navbar />
      <main className="flex-grow">{children}</main>
      <Footer />
    </div>
  );
};

export default MainLayout;
Enter fullscreen mode Exit fullscreen mode

Putting It All Together

Adding the AuthContext to _app.tsx

In pages/_app.tsx, make sure to wrap your application in the AuthProvider context to make authentication available throughout the app.

import { AuthProvider } from '../src/contexts/AuthContext';
import '../src/styles/globals.css';

function MyApp({ Component, pageProps }) {
  return (
    <AuthProvider>
      <Component {...pageProps} />
    </AuthProvider>
  );
}

export default MyApp;
Enter fullscreen mode Exit fullscreen mode

Example Next.js Environment Configuration

Ensure you have an .env.local file (or .env) at the root of your project with your environment variables:

NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/api
Enter fullscreen mode Exit fullscreen mode

Conclusion

This structure and code provide you with a solid foundation for building a scalable and maintainable Car Rental System using Next.js. The combination of Tailwind CSS, custom hooks, contexts, Axios for API requests, and services allows you to separate concerns and maintain clean, reusable code. Let me know if you need further customizations or assistance!

Here’s the full code for the authentication and cars modules in your backend NestJS project, with controllers, services, DTOs, and entity files. This structure is based on the NestJS framework, TypeScript, and TypeORM for database management (PostgreSQL is assumed here).


Project Structure

car-rental-backend/
├── src/
│   ├── auth/
│   │   ├── dto/
│   │   │   ├── login.dto.ts
│   │   │   ├── register.dto.ts
│   │   ├── auth.controller.ts
│   │   ├── auth.module.ts
│   │   ├── auth.service.ts
│   │   ├── jwt.strategy.ts
│   │   └── local.strategy.ts
│   ├── cars/
│   │   ├── dto/
│   │   │   ├── create-car.dto.ts
│   │   │   ├── update-car.dto.ts
│   │   ├── entities/
│   │   │   └── car.entity.ts
│   │   ├── cars.controller.ts
│   │   ├── cars.module.ts
│   │   ├── cars.service.ts
Enter fullscreen mode Exit fullscreen mode

1. auth/dto/login.dto.ts

This DTO validates login request data (username and password).

import { IsString, IsNotEmpty } from 'class-validator';

export class LoginDto {
  @IsString()
  @IsNotEmpty()
  username: string;

  @IsString()
  @IsNotEmpty()
  password: string;
}
Enter fullscreen mode Exit fullscreen mode

2. auth/dto/register.dto.ts

This DTO handles registration by validating the incoming request data for new users.

import { IsString, IsNotEmpty, IsEmail } from 'class-validator';

export class RegisterDto {
  @IsString()
  @IsNotEmpty()
  username: string;

  @IsEmail()
  @IsNotEmpty()
  email: string;

  @IsString()
  @IsNotEmpty()
  password: string;
}
Enter fullscreen mode Exit fullscreen mode

3. auth/auth.controller.ts

This controller handles routes related to authentication, such as login and registration.

import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('login')
  async login(@Body() loginDto: LoginDto) {
    return this.authService.login(loginDto);
  }

  @Post('register')
  async register(@Body() registerDto: RegisterDto) {
    return this.authService.register(registerDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

4. auth/auth.module.ts

The authentication module brings together the controller, service, and strategies for authentication functionality.

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { LocalStrategy } from './local.strategy';

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET || 'secretKey',
      signOptions: { expiresIn: '60m' },
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy, LocalStrategy],
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

5. auth/auth.service.ts

This service contains the logic for user authentication, including login and registration functionality.

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';

@Injectable()
export class AuthService {
  constructor(private readonly jwtService: JwtService) {}

  // Mock user storage (replace with real database later)
  private users = [];

  async login(loginDto: LoginDto) {
    const user = this.users.find(
      (user) => user.username === loginDto.username && user.password === loginDto.password,
    );
    if (!user) {
      throw new Error('Invalid credentials');
    }
    const payload = { username: user.username, sub: user.id };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }

  async register(registerDto: RegisterDto) {
    const user = {
      id: this.users.length + 1,
      ...registerDto,
    };
    this.users.push(user);
    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

6. auth/jwt.strategy.ts

JWT strategy to verify the JWT tokens sent with API requests.

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_SECRET || 'secretKey',
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}
Enter fullscreen mode Exit fullscreen mode

7. auth/local.strategy.ts

Local authentication strategy using username and password.

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.login({ username, password });
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

8. cars/dto/create-car.dto.ts

DTO for creating a new car record.

import { IsString, IsNotEmpty, IsNumber } from 'class-validator';

export class CreateCarDto {
  @IsString()
  @IsNotEmpty()
  name: string;

  @IsString()
  @IsNotEmpty()
  brand: string;

  @IsNumber()
  pricePerDay: number;
}
Enter fullscreen mode Exit fullscreen mode

9. cars/dto/update-car.dto.ts

DTO for updating an existing car record.

import { IsOptional, IsString, IsNumber } from 'class-validator';

export class UpdateCarDto {
  @IsOptional()
  @IsString()
  name?: string;

  @IsOptional()
  @IsString()
  brand?: string;

  @IsOptional()
  @IsNumber()
  pricePerDay?: number;
}
Enter fullscreen mode Exit fullscreen mode

10. cars/entities/car.entity.ts

The TypeORM entity representing a car in the database.

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
export class Car {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  brand: string;

  @Column('decimal')
  pricePerDay: number;

  @Column({ default: true })
  availability: boolean;
}
Enter fullscreen mode Exit fullscreen mode

11. cars/cars.controller.ts

This controller handles routes related to cars, such as listing all cars, getting a car by ID, creating, updating, and deleting a car.

import { Controller, Get, Post, Put, Delete, Param, Body } from '@nestjs/common';
import { CarsService } from './cars.service';
import { CreateCarDto } from './dto/create-car.dto';
import { UpdateCarDto } from './dto/update-car.dto';

@Controller('cars')
export class CarsController {
  constructor(private readonly carsService: CarsService) {}

  @Get()
  findAll() {
    return this.carsService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: number) {
    return this.carsService.findOne(id);
  }

  @Post()
  create(@Body() createCarDto: CreateCarDto) {
    return this.carsService.create(createCarDto);
  }

  @Put(':id')
  update(@Param('id') id: number, @Body() updateCarDto: UpdateCarDto) {
    return this.carsService.update(id, updateCarDto);
  }

  @Delete(':id')
  remove(@Param('id') id: number) {
    return this.carsService.remove(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

12. cars/cars.module.ts

The cars module that brings together the cars controller, service, and entity.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CarsController } from './cars.controller';
import { CarsService } from './cars.service';
import { Car } from './entities/car.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Car])],
  controllers: [CarsController],
  providers: [CarsService],
})
export class CarsModule {}
Enter fullscreen mode Exit fullscreen mode

13. cars/cars.service.ts

This service contains the business logic for managing cars, such

as CRUD operations.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Car } from './entities/car.entity';
import { CreateCarDto } from './dto/create-car.dto';
import { UpdateCarDto } from './dto/update-car.dto';

@Injectable()
export class CarsService {
  constructor(
    @InjectRepository(Car)
    private carsRepository: Repository<Car>,
  ) {}

  findAll(): Promise<Car[]> {
    return this.carsRepository.find();
  }

  findOne(id: number): Promise<Car> {
    return this.carsRepository.findOneBy({ id });
  }

  create(createCarDto: CreateCarDto): Promise<Car> {
    const car = this.carsRepository.create(createCarDto);
    return this.carsRepository.save(car);
  }

  async update(id: number, updateCarDto: UpdateCarDto): Promise<Car> {
    await this.carsRepository.update(id, updateCarDto);
    return this.carsRepository.findOneBy({ id });
  }

  async remove(id: number): Promise<void> {
    await this.carsRepository.delete(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

This code structure provides the authentication and cars modules for your NestJS backend. It handles user authentication with JWT, basic CRUD operations for car management, and uses TypeORM for database interaction with PostgreSQL (or another relational database).

Let me know if you need any further customizations or explanations for specific parts!

Here’s the full code for the bookings and users modules, including DTOs, entities, controllers, and services for your NestJS backend. These modules cover the handling of user creation, booking creation, and updates.

Updated Project Structure

car-rental-backend/
├── src/
│   ├── bookings/
│   │   ├── dto/
│   │   │   ├── create-booking.dto.ts
│   │   │   ├── update-booking.dto.ts
│   │   ├── entities/
│   │   │   └── booking.entity.ts
│   │   ├── bookings.controller.ts
│   │   ├── bookings.module.ts
│   │   ├── bookings.service.ts
│   ├── users/
│   │   ├── dto/
│   │   │   └── create-user.dto.ts
│   │   ├── entities/
│   │   │   └── user.entity.ts
│   │   ├── users.controller.ts
│   │   ├── users.module.ts
│   │   ├── users.service.ts
Enter fullscreen mode Exit fullscreen mode

Bookings Module

1. bookings/dto/create-booking.dto.ts

DTO for creating a new booking.

import { IsDateString, IsNumber, IsNotEmpty } from 'class-validator';

export class CreateBookingDto {
  @IsNumber()
  @IsNotEmpty()
  carId: number;

  @IsNumber()
  @IsNotEmpty()
  userId: number;

  @IsDateString()
  @IsNotEmpty()
  rentalDate: string;

  @IsDateString()
  @IsNotEmpty()
  returnDate: string;
}
Enter fullscreen mode Exit fullscreen mode

2. bookings/dto/update-booking.dto.ts

DTO for updating an existing booking.

import { IsOptional, IsDateString, IsNumber } from 'class-validator';

export class UpdateBookingDto {
  @IsOptional()
  @IsNumber()
  carId?: number;

  @IsOptional()
  @IsNumber()
  userId?: number;

  @IsOptional()
  @IsDateString()
  rentalDate?: string;

  @IsOptional()
  @IsDateString()
  returnDate?: string;
}
Enter fullscreen mode Exit fullscreen mode

3. bookings/entities/booking.entity.ts

The TypeORM entity representing a booking.

import { Entity, PrimaryGeneratedColumn, ManyToOne, Column } from 'typeorm';
import { Car } from '../../cars/entities/car.entity';
import { User } from '../../users/entities/user.entity';

@Entity()
export class Booking {
  @PrimaryGeneratedColumn()
  id: number;

  @ManyToOne(() => Car)
  car: Car;

  @ManyToOne(() => User)
  user: User;

  @Column('date')
  rentalDate: string;

  @Column('date')
  returnDate: string;

  @Column('decimal')
  totalAmount: number;
}
Enter fullscreen mode Exit fullscreen mode

4. bookings/bookings.controller.ts

This controller handles routes related to bookings, such as creating, updating, and retrieving bookings.

import { Controller, Get, Post, Put, Delete, Param, Body } from '@nestjs/common';
import { BookingsService } from './bookings.service';
import { CreateBookingDto } from './dto/create-booking.dto';
import { UpdateBookingDto } from './dto/update-booking.dto';

@Controller('bookings')
export class BookingsController {
  constructor(private readonly bookingsService: BookingsService) {}

  @Get()
  findAll() {
    return this.bookingsService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: number) {
    return this.bookingsService.findOne(id);
  }

  @Post()
  create(@Body() createBookingDto: CreateBookingDto) {
    return this.bookingsService.create(createBookingDto);
  }

  @Put(':id')
  update(@Param('id') id: number, @Body() updateBookingDto: UpdateBookingDto) {
    return this.bookingsService.update(id, updateBookingDto);
  }

  @Delete(':id')
  remove(@Param('id') id: number) {
    return this.bookingsService.remove(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

5. bookings/bookings.module.ts

The bookings module combines the bookings controller, service, and entity.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BookingsService } from './bookings.service';
import { BookingsController } from './bookings.controller';
import { Booking } from './entities/booking.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Booking])],
  controllers: [BookingsController],
  providers: [BookingsService],
})
export class BookingsModule {}
Enter fullscreen mode Exit fullscreen mode

6. bookings/bookings.service.ts

This service contains the business logic for managing bookings.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Booking } from './entities/booking.entity';
import { CreateBookingDto } from './dto/create-booking.dto';
import { UpdateBookingDto } from './dto/update-booking.dto';

@Injectable()
export class BookingsService {
  constructor(
    @InjectRepository(Booking)
    private bookingsRepository: Repository<Booking>,
  ) {}

  findAll(): Promise<Booking[]> {
    return this.bookingsRepository.find({ relations: ['car', 'user'] });
  }

  findOne(id: number): Promise<Booking> {
    return this.bookingsRepository.findOneBy({ id });
  }

  create(createBookingDto: CreateBookingDto): Promise<Booking> {
    const booking = this.bookingsRepository.create(createBookingDto);
    return this.bookingsRepository.save(booking);
  }

  async update(id: number, updateBookingDto: UpdateBookingDto): Promise<Booking> {
    await this.bookingsRepository.update(id, updateBookingDto);
    return this.bookingsRepository.findOneBy({ id });
  }

  async remove(id: number): Promise<void> {
    await this.bookingsRepository.delete(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Users Module

1. users/dto/create-user.dto.ts

DTO for creating a new user.

import { IsEmail, IsString, IsNotEmpty } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @IsNotEmpty()
  username: string;

  @IsEmail()
  @IsNotEmpty()
  email: string;

  @IsString()
  @IsNotEmpty()
  password: string;
}
Enter fullscreen mode Exit fullscreen mode

2. users/entities/user.entity.ts

The TypeORM entity representing a user.

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  username: string;

  @Column()
  email: string;

  @Column()
  password: string;
}
Enter fullscreen mode Exit fullscreen mode

3. users/users.controller.ts

This controller handles routes related to users, such as creating users and fetching user data.

import { Controller, Get, Post, Param, Body } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: number) {
    return this.usersService.findOne(id);
  }

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

4. users/users.module.ts

The users module integrates the users controller, service, and entity.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}
Enter fullscreen mode Exit fullscreen mode

5. users/users.service.ts

This service handles the business logic for user management, such as user creation and retrieval.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  findOne(id: number): Promise<User> {
    return this.usersRepository.findOneBy({ id });
  }

  create(createUserDto: CreateUserDto): Promise<User> {
    const user = this.usersRepository.create(createUserDto);
    return this.usersRepository.save(user);
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

This setup includes the Bookings and Users modules with the full code for creating, updating, and retrieving bookings and users using **NestJS

** with TypeORM for database interaction. The code supports CRUD operations for both modules, making it easy to expand with further functionality.

Let me know if you need more information or help with other modules!

Here's the full code for the payments module and the common utilities like decorators, guards, interceptors, and filters. This structure is for a NestJS backend application.

Updated Project Structure

car-rental-backend/
├── src/
│   ├── payments/
│   │   ├── dto/
│   │   │   └── create-payment.dto.ts
│   │   ├── payments.controller.ts
│   │   ├── payments.module.ts
│   │   ├── payments.service.ts
│   ├── common/
│   │   ├── decorators/
│   │   │   └── roles.decorator.ts
│   │   ├── guards/
│   │   │   └── jwt-auth.guard.ts
│   │   ├── interceptors/
│   │   │   └── logging.interceptor.ts
│   │   ├── filters/
│   │   │   └── http-exception.filter.ts
│   │   └── constants.ts
Enter fullscreen mode Exit fullscreen mode

Payments Module

The Payments module handles payment processing in your system, such as creating a payment for a car rental. This might integrate with payment gateways like Stripe or PayPal.


1. payments/dto/create-payment.dto.ts

This DTO defines the data structure for creating a new payment.

import { IsNumber, IsNotEmpty, IsString } from 'class-validator';

export class CreatePaymentDto {
  @IsNumber()
  @IsNotEmpty()
  bookingId: number;

  @IsNumber()
  @IsNotEmpty()
  amount: number;

  @IsString()
  @IsNotEmpty()
  paymentMethod: string; // e.g., 'credit_card', 'paypal'
}
Enter fullscreen mode Exit fullscreen mode

2. payments/payments.controller.ts

This controller manages the HTTP routes related to payments.

import { Controller, Post, Body } from '@nestjs/common';
import { PaymentsService } from './payments.service';
import { CreatePaymentDto } from './dto/create-payment.dto';

@Controller('payments')
export class PaymentsController {
  constructor(private readonly paymentsService: PaymentsService) {}

  @Post()
  create(@Body() createPaymentDto: CreatePaymentDto) {
    return this.paymentsService.processPayment(createPaymentDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

3. payments/payments.module.ts

The Payments module groups together the payments controller and service.

import { Module } from '@nestjs/common';
import { PaymentsService } from './payments.service';
import { PaymentsController } from './payments.controller';

@Module({
  controllers: [PaymentsController],
  providers: [PaymentsService],
})
export class PaymentsModule {}
Enter fullscreen mode Exit fullscreen mode

4. payments/payments.service.ts

The PaymentsService handles the business logic for payment processing, potentially integrating with third-party services like Stripe.

import { Injectable } from '@nestjs/common';
import { CreatePaymentDto } from './dto/create-payment.dto';

@Injectable()
export class PaymentsService {
  processPayment(createPaymentDto: CreatePaymentDto) {
    // This is where you'd integrate with a payment gateway (e.g., Stripe, PayPal)
    // For demonstration, we'll just return a successful payment object.
    return {
      id: Math.floor(Math.random() * 1000),
      bookingId: createPaymentDto.bookingId,
      amount: createPaymentDto.amount,
      paymentMethod: createPaymentDto.paymentMethod,
      status: 'success',
      transactionId: `txn_${Math.random().toString(36).substr(2, 9)}`,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Common Module

This module contains shared utilities such as decorators, guards, interceptors, and filters that can be reused throughout the application.


1. common/decorators/roles.decorator.ts

This decorator is used to restrict routes to specific roles.

import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
Enter fullscreen mode Exit fullscreen mode

2. common/guards/jwt-auth.guard.ts

This guard protects routes by verifying the validity of a JWT token.

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
Enter fullscreen mode Exit fullscreen mode

To use this guard, ensure that your JWT strategy is implemented (as shown earlier).


3. common/interceptors/logging.interceptor.ts

This interceptor logs requests and responses, which is useful for debugging and monitoring.

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before handling request:', context.switchToHttp().getRequest().url);
    const now = Date.now();
    return next.handle().pipe(
      tap(() =>
        console.log(
          `After handling request in ${Date.now() - now}ms`,
          context.switchToHttp().getResponse().statusCode,
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

4. common/filters/http-exception.filter.ts

This filter is used for handling and formatting HTTP exceptions (errors).

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const errorResponse = {
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: exception.message || null,
    };

    response.status(status).json(errorResponse);
  }
}
Enter fullscreen mode Exit fullscreen mode

5. common/constants.ts

This file contains constants that can be reused across the application, such as role definitions, API keys, or status codes.

export const JWT_SECRET = process.env.JWT_SECRET || 'secretKey';

export const USER_ROLES = {
  ADMIN: 'admin',
  USER: 'user',
};

export const STATUS_CODES = {
  SUCCESS: 'success',
  FAILURE: 'failure',
};
Enter fullscreen mode Exit fullscreen mode

Putting It All Together

You can apply these common utilities across your application. For instance:

  • Role-based Access Control: You can use the @Roles() decorator and JwtAuthGuard to restrict certain routes.
  • Logging: Apply the LoggingInterceptor globally or at the controller level to monitor request handling.
  • Error Handling: Use the HttpExceptionFilter to standardize error responses across your application.

Example: Securing a Controller with Role-Based Access Control

import { Controller, Get, UseGuards } from '@nestjs/common';
import { Roles } from '../common/decorators/roles.decorator';
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';

@Controller('admin')
@UseGuards(JwtAuthGuard)
export class AdminController {
  @Get()
  @Roles('admin')
  getAdminData() {
    return { message: 'This is secured admin data' };
  }
}
Enter fullscreen mode Exit fullscreen mode

This ensures that only authenticated users with the role of 'admin' can access this route.


Conclusion

This provides the full code for the Payments module and shared Common utilities like decorators, guards, interceptors, and filters. These utilities are essential for securing and standardizing your NestJS application, making it more maintainable and scalable. If you need further adjustments or explanation for specific parts, feel free to ask!

Here is the full code for the database module, app entry files, config files, and migrations for your NestJS backend application. This includes the setup for TypeORM, JWT, and global application configurations.


Database Module

The database module is responsible for managing the connection to the database, including the base entity and service files for database interaction.


1. database/entities/base.entity.ts

This is a base entity that can be extended by other entities to include common fields like id, createdAt, and updatedAt.

import {
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  BaseEntity,
} from 'typeorm';

export abstract class Base extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @CreateDateColumn({ type: 'timestamp' })
  createdAt: Date;

  @UpdateDateColumn({ type: 'timestamp' })
  updatedAt: Date;
}
Enter fullscreen mode Exit fullscreen mode

2. database/database.module.ts

The DatabaseModule imports and provides the TypeORM connection to the rest of the application.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmConfigService } from '../config/typeorm.config';

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useClass: TypeOrmConfigService,
    }),
  ],
})
export class DatabaseModule {}
Enter fullscreen mode Exit fullscreen mode

3. database/database.service.ts

The DatabaseService can be used for managing database connections and performing additional database-related operations if needed.

import { Injectable } from '@nestjs/common';

@Injectable()
export class DatabaseService {
  // Any additional database utilities or services can be added here.
}
Enter fullscreen mode Exit fullscreen mode

App Entry Files

These are the core entry points of your NestJS application: app.module.ts and main.ts.


1. app.module.ts

The AppModule imports all necessary modules and serves as the root module for the application.

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './database/database.module';
import { AuthModule } from './auth/auth.module';
import { CarsModule } from './cars/cars.module';
import { BookingsModule } from './bookings/bookings.module';
import { PaymentsModule } from './payments/payments.module';
import { UsersModule } from './users/users.module';
import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
    DatabaseModule,
    AuthModule,
    CarsModule,
    BookingsModule,
    PaymentsModule,
    UsersModule,
  ],
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor,
    },
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

2. main.ts

The main.ts file bootstraps the NestJS application.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  app.useGlobalFilters(new HttpExceptionFilter());
  app.enableCors();
  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Config Module

The config module manages all configuration settings, including JWT and TypeORM configurations.


1. config/jwt.config.ts

This file contains the configuration for JWT authentication.

import { Injectable } from '@nestjs/common';
import { JwtModuleOptions, JwtOptionsFactory } from '@nestjs/jwt';

@Injectable()
export class JwtConfigService implements JwtOptionsFactory {
  createJwtOptions(): JwtModuleOptions {
    return {
      secret: process.env.JWT_SECRET || 'defaultSecret',
      signOptions: { expiresIn: '60m' },
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

2. config/typeorm.config.ts

This file contains the configuration for TypeORM.

import { Injectable } from '@nestjs/common';
import { TypeOrmOptionsFactory, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
  constructor(private configService: ConfigService) {}

  createTypeOrmOptions(): TypeOrmModuleOptions {
    return {
      type: 'postgres',
      host: this.configService.get<string>('DB_HOST'),
      port: this.configService.get<number>('DB_PORT'),
      username: this.configService.get<string>('DB_USERNAME'),
      password: this.configService.get<string>('DB_PASSWORD'),
      database: this.configService.get<string>('DB_NAME'),
      autoLoadEntities: true,
      synchronize: true, // In production, use migrations instead of synchronize: true
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

3. config/app.config.ts

Global application settings, such as CORS configuration or app-wide constants, can be defined here.

export const APP_CONFIG = {
  cors: {
    origin: '*',
    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
    preflightContinue: false,
    optionsSuccessStatus: 204,
  },
};
Enter fullscreen mode Exit fullscreen mode

Migrations

Migrations allow you to manage changes to the database schema over time. TypeORM generates these migrations.


1. migrations/1682570845798-CreateCarEntity.ts

A migration file to create the Car entity.

import { MigrationInterface, QueryRunner, Table } from 'typeorm';

export class CreateCarEntity1682570845798 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.createTable(
      new Table({
        name: 'car',
        columns: [
          {
            name: 'id',
            type: 'int',
            isPrimary: true,
            isGenerated: true,
            generationStrategy: 'increment',
          },
          {
            name: 'name',
            type: 'varchar',
          },
          {
            name: 'brand',
            type: 'varchar',
          },
          {
            name: 'pricePerDay',
            type: 'decimal',
          },
          {
            name: 'availability',
            type: 'boolean',
            default: true,
          },
          {
            name: 'createdAt',
            type: 'timestamp',
            default: 'now()',
          },
          {
            name: 'updatedAt',
            type: 'timestamp',
            default: 'now()',
          },
        ],
      }),
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.dropTable('car');
  }
}
Enter fullscreen mode Exit fullscreen mode

2. migrations/1682570845799-CreateBookingEntity.ts

A migration file to create the Booking entity.

import { MigrationInterface, QueryRunner, Table } from 'typeorm';

export class CreateBookingEntity1682570845799 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.createTable(
      new Table({
        name: 'booking',
        columns: [
          {
            name: 'id',
            type: 'int',
            isPrimary: true,
            isGenerated: true,
            generationStrategy: 'increment',
          },
          {
            name: 'carId',
            type: 'int',
          },
          {
            name: 'userId',
            type: 'int',
          },
          {
            name: 'rentalDate',
            type: 'date',
          },
          {
            name: 'returnDate',
            type: 'date',
          },
          {
            name: 'totalAmount',
            type: 'decimal',
          },
          {
            name: 'createdAt',
            type: 'timestamp',
            default: 'now()',
          },
          {
            name: 'updatedAt',
            type: 'timestamp',
            default: 'now()',
          },
        ],
      }),
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.dropTable('booking');
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

This complete structure includes:

  • Database setup with TypeORM and base entity definitions.
  • AppModule that imports all other modules and configures the application globally.
  • Configuration for JWT, TypeORM, and global application settings.
  • Migrations for managing database schema changes.

This structure ensures that your NestJS application is modular, scalable, and easy to manage with clear separation between different concerns. Let me know if you need further clarifications or help with specific parts!

If you enjoy my content and would like to support my work, you can buy me a coffee. Your support is greatly appreciated!

Disclaimer: This content is generated by AI.

Top comments (0)