Skip to content

Carhub is a simple demo project allowing users to create and view cars, but the interesting part is the tech stack used - using a monorepo, zod and tRPC/Swagger to solve commen HTTP API headaches.

Notifications You must be signed in to change notification settings

williamwinkler/carhub

Repository files navigation

Carhub

Carhub is a simple demo project allowing users to create and view cars, but the interesting part is the tech stack used.

The project demonstrates end-to-end type safety and auto-generated documentation using NestJS, tRPC, Swagger and Zod. The project's goal is to have top tier DX when working with APIs - both in the front and backend.

License: MIT

πŸ—οΈ Architecture Overview

test

πŸ§‘β€πŸ’» Improved DX

Improved Nestjs Swagger Documentation

  • Auto-generated DTOs from Zod schemas
  • Compile time error if the Swagger specified DTO and actual DTO are different!
  • Custom decorators (@zQuery, @zParam) for endpoint parameter validation - automatically shows up in swagger too.
  • Specified errors your app can throw and each will show up in the documentation.
  • Common errors are automatically derived in the swagger docs:
    • Each endpoint automatically gets 429 and 500 error examples added.
    • If the endpoint performs any validation, 400 Valiation Error is automatically added.
    • Protected endpoints gets the 401 Unauthorized and 403 Forbidden automatically added.
  • See code examples below!

Better feature development loop

  • It's faster and more reliable with tRPC.
    • Types are procedures are immediately updated in the frontend - no codegen needed.
    • Jump between frontend and backend code easily.
    • No more "Where is this endpoint used?" - see directly where each tRPC prodecure is used in the frontend.
    • Backend errors are automatically converted to tRPC errors in middleware.
  • A postgres DB and pgadmin are automatically started when starting to develop with pnpm dev.

⚑ Quick Start

Prerequisites

  • Node.js 24+
  • Docker installed and running
  • pnpm (npm install -g pnpm)

Installation (Development)

# 1. Clone the repository
git clone https://github.com/williamwinkler/carhub.git
cd carhub

# 2. Install dependencies
pnpm install

# 3. Setup environment files
cp apps/api/.env.example apps/api/.env.local
cp apps/web/.env.example apps/web/.env.local

# 4. Configure your environment files (recommended)
pnpm build:packages

# 5. Start the development database
docker compose up -d

# 6. Run database migrations
pnpm --filter api migrations:run

# (optional) Seed with sample data
pnpm --filter api seed

# Start both the frontend and backend
pnpm dev

Sample Data Created

  • 2 Users: admin/admin and jondoe/jondoe
  • 10 Manufacturers: Toyota, Honda, Ford, BMW, Mercedes-Benz, etc.
  • 50 Car Models: 5 models per manufacturer with slugs
  • Full Relationships: Proper foreign keys and cascading

πŸ“– API Swagger Documentation

Once running, visit:

🎯 Code Examples

Schema-First Development

// 1. Define Zod schema (single source of truth)
export const createCarSchema = z.object({
  modelId: z.string().uuid(),
  color: z.string().min(1).max(50),
  year: z.number().int().gte(1900),
  price: z.number().min(0),
});

// 2. Auto-generate DTO for Swagger
export class CreateCarDto extends createZodDto(createCarSchema) {}

// 3. Use in controller with automatic validation
@Post()
@SwaggerInfo({
  status: 201,
  summary: "Create a car",
  successText: "Car was succesfully created",
  type: CarDto, // <- Won't compile if a CarDto is not returned!
  errors: [Errors.SOME_ERROR]
})
async create(@Body() dto: CreateCarDto) {
  // dto is validated and typed automatically!
}

Custom Validation Decorators

@Get(":carId")
async findCars(
  @zParam("carId", z.uuid()) carId: UUID,
  @zQuery("color", z.string().optional()) color?: string,
  @zQuery("minPrice", z.number().min(0).optional()) minPrice?: number,
  @zQuery("skip", z.number().int().gte(0).default(0)) skip = 0,
) {
  // All parameters validated automatically
  // Swagger docs generated automatically
}

tRPC Type Safety

// Backend tRPC procedure
getById: procedure
  .input(z.object({ id: z.uuid() })) // id as UUID required
  .query(async ({ input: { id } }) => {
    const car = await this.carsService.findById(id);

    if (!car) {
      // API specific errors are automatically converted to tRPC errors
      throw new AppError(Errors.CAR_NOT_FOUND)
    }

    return this.carsAdapter.getDto(car);
  });

// Frontend usage (fully typed!)
const car = await trpc.cars.getById.query({
  id: "<UUID>",     // βœ… Typed
});
// Response is automatically typed as well!

πŸ“„ License

MIT - Feel free to use this as inspiration for your own projects!

About

Carhub is a simple demo project allowing users to create and view cars, but the interesting part is the tech stack used - using a monorepo, zod and tRPC/Swagger to solve commen HTTP API headaches.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages