Vercel Logo

Add shared utils

We're formatting dates with toLocaleDateString() in apps/snippet-manager. Utility functions like date formatting, text slugification, and string truncation are perfect candidates for shared packages - they're pure functions with no UI dependencies.

You'll create packages/utils with common utilities that work across all apps. This demonstrates that monorepos aren't just for sharing components - you can share any code.

Outcome

Create packages/utils with utility functions and use formatDate in the snippet manager.

Fast track

  1. Create packages/utils package structure
  2. Add utility functions (formatDate, slugify, truncate, validateEmail)
  3. Export functions from package
  4. Use formatDate in snippet manager app

Hands-on exercise 4.2

Create shared utilities package with common helper functions.

Requirements:

  1. Create packages/utils directory****
  2. Add package.json with TypeScript config
  3. Create src/index.ts with utility functions
  4. Export formatDate, slugify, truncate, validateEmail
  5. Add utils dependency to apps/snippet-manager
  6. Replace date formatting with formatDate utility
  7. Test in both dev and build

Implementation hints:

  • Use Intl.DateTimeFormat for formatDate
  • slugify converts text to URL-safe format
  • truncate limits string length with ellipsis
  • validateEmail uses regex pattern
  • Export all functions from src/index.ts

Create utils package

Create the directory structure:

mkdir -p packages/utils/src

Create packages/utils/package.json. Not in the src directory but in utils:

packages/utils/package.json
{
  "name": "@geniusgarage/utils",
  "version": "1.0.0",
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": {
    ".": "./src/index.ts"
  },
  "scripts": {
    "lint": "eslint ."
  },
  "devDependencies": {
    "@geniusgarage/typescript-config": "workspace:*",
    "@geniusgarage/eslint-config": "workspace:*",
    "eslint": "^10",
    "typescript": "^5"
  }
}

Create packages/utils/tsconfig.json:

packages/utils/tsconfig.json
{
  "extends": "@geniusgarage/typescript-config/base.json",
  "compilerOptions": {
    "outDir": "./dist"
  },
  "include": ["src"]
}

Notice it extends the shared TypeScript config from packages/typescript-config!

Create utility functions

Create packages/utils/src/index.ts:

packages/utils/src/index.ts
// TODO: Export formatDate function that:
//   - Takes a Date parameter
//   - Returns formatted string "Jan 15, 2024"
//   - Uses Intl.DateTimeFormat with en-US, month: 'short', day: 'numeric', year: 'numeric'
 
// TODO: Export slugify function that:
//   - Takes a string parameter
//   - Returns URL-safe slug (lowercase, replace spaces with hyphens, remove special chars)
//   - Example: "Hello World!" → "hello-world"
 
// TODO: Export truncate function that:
//   - Takes text string and maxLength number
//   - Returns truncated string with "..." if longer than maxLength
//   - Example: truncate("Hello World", 5) → "Hello..."
 
// TODO: Export validateEmail function that:
//   - Takes email string
//   - Returns boolean (true if valid email format)
//   - Uses regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/

Your task: Implement all four utility functions.

Hints:

  • formatDate: new Intl.DateTimeFormat('en-US', { ... }).format(date)
  • slugify: Chain .toLowerCase(), .replace() calls
  • truncate: Check text.length <= maxLength, use text.slice(0, maxLength) + '...'
  • validateEmail: return regex.test(email)
Solution: if you didn't come here to write functions
packages/utils/src/index.ts
export function formatDate(date: Date): string {
  return new Intl.DateTimeFormat('en-US', {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
  }).format(date)
}
 
export function slugify(text: string): string {
  return text
    .toLowerCase()
    .replace(/[^\w\s-]/g, '')
    .replace(/\s+/g, '-')
    .replace(/-+/g, '-')
    .trim()
}
 
export function truncate(text: string, maxLength: number): string {
  if (text.length <= maxLength) return text
  return text.slice(0, maxLength) + '...'
}
 
export function validateEmail(email: string): boolean {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  return regex.test(email)
}

Add utils to snippet manager

Install the utils package:

pnpm add @geniusgarage/utils --filter @geniusgarage/snippet-manager --workspace
pnpm install

Update snippet interface

Open apps/snippet-manager/app/page.tsx and change createdAt from string to Date:

apps/snippet-manager/app/page.tsx
interface Snippet {
  id: number
  title: string
  language: string
  code: string
  tags: string[]
  createdAt: Date  // Changed from string to Date
}

Import formatdate

Add the import at the top:

apps/snippet-manager/app/page.tsx
import { useState } from 'react'
import { Button } from '@geniusgarage/ui/button'
import { SnippetCard } from '@geniusgarage/ui/snippet-card'
import { formatDate } from '@geniusgarage/utils'

Update mock data

Change the mock data to use Date objects:

apps/snippet-manager/app/page.tsx
const initialSnippets: Snippet[] = [
  {
    id: 1,
    title: 'Array Reduce Pattern',
    language: 'javascript',
    code: 'const sum = arr.reduce((acc, n) => acc + n, 0)',
    tags: ['javascript', 'array', 'functional'],
    createdAt: new Date('2024-01-15'),  // Date object
  },
  {
    id: 2,
    title: 'React useEffect Cleanup',
    language: 'typescript',
    code: `useEffect(() => {
  const timer = setTimeout(() => {}, 1000)
  return () => clearTimeout(timer)
}, [])`,
    tags: ['react', 'hooks', 'typescript'],
    createdAt: new Date('2024-02-20'),  // Date object
  },
  {
    id: 3,
    title: 'Promise.all Pattern',
    language: 'javascript',
    code: 'const results = await Promise.all(promises.map(p => p()))',
    tags: ['javascript', 'async', 'promises'],
    createdAt: new Date('2024-03-10'),  // Date object
  },
]

Update handlecreatesnippet

Replace the date formatting with formatDate utility:

apps/snippet-manager/app/page.tsx
const handleCreateSnippet = () => {
  if (!newSnippet.title || !newSnippet.code) return
 
  const snippet: Snippet = {
    id: Date.now(),
    title: newSnippet.title,
    language: newSnippet.language,
    code: newSnippet.code,
    tags: newSnippet.tags.split(',').map(t => t.trim()).filter(Boolean),
    createdAt: new Date()  // Now a Date object
  }
 
  setSnippets([snippet, ...snippets])
  setShowModal(false)
  setNewSnippet({ title: '', language: 'javascript', code: '', tags: '' })
}

Update snippetcard rendering

Pass formatted date to SnippetCard:

apps/snippet-manager/app/page.tsx
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
  {snippets.map((snippet) => (
    <SnippetCard
      key={snippet.id}
      title={snippet.title}
      language={snippet.language}
      code={snippet.code}
      tags={snippet.tags}
      createdAt={formatDate(snippet.createdAt)}
    />
  ))}
</div>

Now dates are formatted consistently using the shared utility!

Try it

1. Test in development

pnpm --filter @geniusgarage/snippet-manager dev

Open http://localhost:3001:

  • Initial snippets show formatted dates: "Jan 15, 2024", "Feb 20, 2024", "Mar 10, 2024"
  • Create a new snippet - it gets today's date formatted consistently

2. Test utility functions in console

While dev server is running, open browser console and test:

// formatDate is used in the app
// slugify example:
"Hello World!".toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-')
// → "hello-world"
 
// truncate example:
"This is a very long text".slice(0, 10) + "..."
// → "This is a ..."

The utilities work!

3. Build and verify

turbo build

Output:

@geniusgarage/web:build: cache miss, executing 5.123s
@geniusgarage/snippet-manager:build: cache miss, executing 4.891s

Tasks:    2 successful, 2 total
Cached:   0 cached, 2 total
Time:     5.234s

Notice:

  • No utils task. packages/utils exports ./src/index.ts and has no build script, so the snippet-manager build compiles it
  • snippet-manager rebuilt (imports from utils)
  • web rebuilt too, even though it doesn't use utils yet

The web rebuild is expected. Last lesson changed its tsconfig.json and package.json, and both are part of its hash. Run turbo build again and both apps replay from cache.

How shared utils work

Your monorepo now shares utilities:

  packages/utils/
  └── src/index.ts        ← Utility functions
          ↓
      formatDate()
          ↓
      apps/snippet-manager
      (imports and uses)

When to create shared packages:

  • UI components → packages/ui (Button, Card, etc.)
  • TypeScript config → packages/typescript-config (base.json, nextjs.json)
  • ESLint config → packages/eslint-config (shared linting rules)
  • Utility functions → packages/utils (formatDate, slugify)
  • Business logic → packages/core (future: snippet validation, etc.)

Benefits:

  • Single source - formatDate defined once, used everywhere
  • Easy testing - Test pure functions in isolation
  • Reusable - Any app can import from packages/utils
  • Type-safe - Full TypeScript support across workspace

Commit

git add .
git commit -m "feat(utils): add shared utilities package"

Done-when

Verify shared utilities work:

  • Created packages/utils/src directory
  • Added package.json with TypeScript and ESLint config dependencies
  • Created tsconfig.json extending @geniusgarage/typescript-config/base.json
  • Implemented formatDate function with Intl.DateTimeFormat
  • Implemented slugify function with string transformations
  • Implemented truncate function with maxLength check
  • Implemented validateEmail function with regex
  • Exported all functions from src/index.ts
  • Added @geniusgarage/utils dependency to apps/snippet-manager
  • Imported formatDate in apps/snippet-manager/app/page.tsx
  • Changed Snippet interface createdAt to Date type
  • Updated mock data to use Date objects
  • Updated handleCreateSnippet to use Date object
  • Passed formatDate(snippet.createdAt) to SnippetCard
  • Tested in dev and saw formatted dates
  • Built with turbo and saw snippet-manager rebuild with utils compiled in (no separate utils task)

What's next

You've created three shared packages (ui, config, utils), but Turborepo doesn't know about their dependency relationships. Next lesson: Update Turbo Pipeline - configure task dependencies so Turborepo builds packages in the correct order and caches effectively.

Was this helpful?

supported.