Package System

Working with shared packages in Portfolio OS

Package System

Portfolio OS uses shared packages to promote code reuse and maintain consistency across applications.

Package Overview

UI

Shared React components with Radix UI primitives

Lib

Business logic, API clients, and services

Utils

Pure utility functions and helpers

DB

Prisma schema and database client

Hashnode

Hashnode API integration

Emails

Transactional email templates

UI Package

Location: packages/ui

Shared React components built with Radix UI and Tailwind CSS.

Structure

Example Component

import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"

const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
        secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
        icon: "h-10 w-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, ...props }, ref) => {
    return (
      <button
        className={buttonVariants({ variant, size, className })}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"

Usage

import { Button } from "@mindware-blog/ui"

export function Hero() {
  return (
    <div>
      <h1>Welcome</h1>
      <Button variant="default" size="lg">
        Get Started
      </Button>
      <Button variant="outline">
        Learn More
      </Button>
    </div>
  )
}

Available Components

ComponentDescriptionPrimitives
ButtonAccessible button with variants-
CardContent container-
DialogModal dialogsRadix Dialog
DropdownMenuContextual menusRadix Dropdown
InputForm input-
SelectDropdown selectRadix Select
TabsTab navigationRadix Tabs
ToastNotificationsRadix Toast

Lib Package

Location: packages/lib

Business logic, API clients, and service integrations.

Structure

Example: Hashnode Client

const HASHNODE_API_URL = "https://gql.hashnode.com"

export interface Post {
  id: string
  title: string
  slug: string
  brief: string
  content: { html: string }
  publishedAt: string
}

export async function getAllPosts(): Promise<Post[]> {
  const query = `
    query GetPosts($host: String!) {
      publication(host: $host) {
        posts(first: 20) {
          edges {
            node {
              id
              title
              slug
              brief
              content { html }
              publishedAt
            }
          }
        }
      }
    }
  `
  
  const response = await fetch(HASHNODE_API_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      query,
      variables: {
        host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST,
      },
    }),
  })
  
  const data = await response.json()
  return data.data.publication.posts.edges.map((edge: any) => edge.node)
}

export async function getPostBySlug(slug: string): Promise<Post | null> {
  // Implementation
}

Example: Cache Service

import { Redis } from "@upstash/redis"

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})

export async function getCached<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttl: number = 3600
): Promise<T> {
  // Try to get from cache
  const cached = await redis.get<T>(key)
  if (cached) return cached
  
  // Fetch fresh data
  const data = await fetcher()
  
  // Cache it
  await redis.setex(key, ttl, JSON.stringify(data))
  
  return data
}

Usage

import { getAllPosts } from "@mindware-blog/lib/hashnode"
import { getCached } from "@mindware-blog/lib/cache"

export default async function BlogPage() {
  const posts = await getCached(
    "blog:posts:all",
    () => getAllPosts(),
    3600 // 1 hour
  )
  
  return (
    <div>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  )
}

Utils Package

Location: packages/utils

Pure utility functions without external dependencies.

Structure

Example Utilities

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

/**
 * Merge Tailwind CSS classes
 */
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Usage:

<div className={cn(
  "base-class",
  isActive && "active-class",
  className
)} />

DB Package

Location: packages/db

Prisma schema and database client for the dashboard.

Structure

Schema

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String
  published Boolean  @default(false)
  slug      String   @unique
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Media {
  id        String   @id @default(cuid())
  url       String
  filename  String
  size      Int
  type      String
  createdAt DateTime @default(now())
}

Client Export

import { PrismaClient } from "@prisma/client"

declare global {
  var prisma: PrismaClient | undefined
}

export const prisma = global.prisma || new PrismaClient()

if (process.env.NODE_ENV !== "production") {
  global.prisma = prisma
}

Usage

import { prisma } from "@mindware-blog/db"

export async function getPosts() {
  return await prisma.post.findMany({
    where: { published: true },
    orderBy: { createdAt: "desc" },
  })
}

export async function createPost(data: {
  title: string
  content: string
  slug: string
}) {
  return await prisma.post.create({ data })
}

Hashnode Package

Location: packages/hashnode

Dedicated Hashnode API integration.

export { getAllPosts, getPostBySlug } from "./client"
export type { Post, Author, Publication } from "./types"
export { GET_ALL_POSTS_QUERY, GET_POST_QUERY } from "./queries"

Creating a New Package

# Create directory
mkdir -p packages/my-package/src
cd packages/my-package

# Create files
touch package.json
touch tsconfig.json
touch src/index.ts

Package Best Practices

1. Single Responsibility

Each package should do one thing well:

  • UI: Only React components
  • Lib: Business logic and API clients
  • Utils: Pure functions only

2. Minimal Dependencies

Keep packages lightweight:

{
  "dependencies": {
    // Only essential dependencies
    "@mindware-blog/utils": "workspace:*"
  }
}

3. Proper Exports

Export only public APIs:

// ✅ Good
export { Button } from "./button"
export type { ButtonProps } from "./button"

// ❌ Bad - don't export internal helpers
export { internalHelper } from "./internal"

4. Type Safety

Provide comprehensive types:

export interface UserService {
  getUser(id: string): Promise<User>
  updateUser(id: string, data: Partial<User>): Promise<User>
}

5. Documentation

Document public APIs with JSDoc:

/**
 * Formats a date for display
 * @param date - Date to format
 * @returns Formatted date string
 */
export function formatDate(date: Date): string {
  // Implementation
}

Versioning

Packages use workspace:* protocol, so versioning is managed at the monorepo level.

When releasing:

  1. Update root package.json version
  2. Create git tag
  3. Deploy apps

No need to publish packages to npm unless making them public.

Testing Packages

import { render, screen } from "@testing-library/react"
import { Button } from "../src/button"

describe("Button", () => {
  it("renders correctly", () => {
    render(<Button>Click me</Button>)
    expect(screen.getByText("Click me")).toBeInTheDocument()
  })
})

Run tests:

# Test specific package
pnpm test --filter=@mindware-blog/ui

# Test all packages
pnpm test --filter="./packages/*"

Next Steps

Note:

Well-structured packages are the foundation of a maintainable monorepo. Keep them focused, documented, and tested.