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
- Create packages/utils package structure
- Add utility functions (formatDate, slugify, truncate, validateEmail)
- Export functions from package
- Use formatDate in snippet manager app
Hands-on exercise 4.2
Create shared utilities package with common helper functions.
Requirements:
- Create
packages/utilsdirectory**** - Add package.json with TypeScript config
- Create
src/index.tswith utility functions - Export formatDate, slugify, truncate, validateEmail
- Add utils dependency to apps/snippet-manager
- Replace date formatting with formatDate utility
- 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/srcCreate packages/utils/package.json. Not in the src directory but in utils:
{
"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:
{
"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:
// 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, usetext.slice(0, maxLength) + '...' - validateEmail:
return regex.test(email)
Solution: if you didn't come here to write functions
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 installUpdate snippet interface
Open apps/snippet-manager/app/page.tsx and change createdAt from string to Date:
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:
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:
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:
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:
<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 devOpen 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 buildOutput:
@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.tsand has nobuildscript, 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/srcdirectory - Added package.json with TypeScript and ESLint config dependencies
- Created tsconfig.json extending
@geniusgarage/typescript-config/base.json - Implemented
formatDatefunction with Intl.DateTimeFormat - Implemented
slugifyfunction 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?