Overview
This quickstart guide will walk you through building a complete blogging application with Esix. You’ll learn how to define models, perform CRUD operations, query data, and work with relationships.
By the end of this tutorial, you’ll have a working blog application with authors, posts, and the ability to query, filter, and aggregate data.
Prerequisites
Before you begin, make sure you have:
- Node.js 20 or higher installed
- MongoDB 4.0+ running locally or a MongoDB Atlas account
- Basic TypeScript knowledge
Installation
Install Esix and MongoDB driver:
Configuration
Set your MongoDB connection string as an environment variable:
export DB_URL=mongodb://localhost:27017/blog
For MongoDB Atlas:
export DB_URL=mongodb+srv://username:password@cluster.mongodb.net/blog
That’s it! No configuration files, no decorators—just set your connection string and you’re ready to go.
Define Your Models
Create a file called models.ts and define your data models by extending BaseModel:
import { BaseModel } from 'esix'
class Author extends BaseModel {
public name = ''
public email = ''
public bio = ''
public isActive = true
// Define relationship to posts
posts() {
return this.hasMany(Post, 'authorId')
}
}
class Post extends BaseModel {
public title = ''
public content = ''
public authorId = ''
public tags: string[] = []
public views = 0
public publishedAt: Date | null = null
}
export { Author, Post }
Every model automatically includes id, createdAt, and updatedAt fields. You don’t need to define them.
Create Records
Let’s create some authors and posts:
import { Author, Post } from './models'
// Create an author
const author = await Author.create({
name: 'Jane Doe',
email: 'jane@example.com',
bio: 'Technology writer and blogger'
})
console.log(`Created author with ID: ${author.id}`)
// Create posts for this author
const post1 = await Post.create({
title: 'Getting Started with TypeScript',
content: 'TypeScript is a powerful superset of JavaScript...',
authorId: author.id,
tags: ['typescript', 'programming'],
publishedAt: new Date()
})
const post2 = await Post.create({
title: 'MongoDB Best Practices',
content: 'Learn how to structure your MongoDB databases...',
authorId: author.id,
tags: ['mongodb', 'database'],
publishedAt: new Date()
})
console.log(`Created ${2} posts`)
Query Data
Now let’s retrieve and filter our data:
Find by ID
// Find a specific author
const foundAuthor = await Author.find(author.id)
console.log(`Found: ${foundAuthor?.name}`)
// Find a specific post
const foundPost = await Post.find(post1.id)
console.log(`Found: ${foundPost?.title}`)
Find by Field
// Find author by email
const author = await Author.findBy('email', 'jane@example.com')
console.log(`Author: ${author?.name}`)
Get All Records
// Get all authors
const allAuthors = await Author.all()
console.log(`Total authors: ${allAuthors.length}`)
// Get all posts
const allPosts = await Post.all()
console.log(`Total posts: ${allPosts.length}`)
Filter with Where Clauses
Use the where() method to filter records:
Equality Filters
// Find active authors
const activeAuthors = await Author.where('isActive', true).get()
// Find posts with specific tag
const typescriptPosts = await Post.where('tags', ['typescript', 'programming']).get()
Comparison Operators
Esix supports all standard comparison operators:
// Posts with more than 100 views
const popularPosts = await Post.where('views', '>', 100).get()
// Posts with at least 50 views
const moderatelyPopular = await Post.where('views', '>=', 50).get()
// Posts with fewer than 10 views
const lowViewPosts = await Post.where('views', '<', 10).get()
// Posts that are not null
const publishedPosts = await Post.where('publishedAt', '!=', null).get()
Chaining Conditions
Combine multiple conditions for complex queries:
// Find published posts with high views
const topPublishedPosts = await Post
.where('publishedAt', '!=', null)
.where('views', '>', 100)
.get()
// Find active authors with specific email domain
const companyAuthors = await Author
.where('isActive', true)
.where('email', 'jane@example.com')
.get()
Array Queries
Query records where a field matches any value in an array:
// Find posts by multiple IDs
const specificPosts = await Post
.whereIn('id', [post1.id, post2.id])
.get()
// Find posts excluding certain tags
const filteredPosts = await Post
.whereNotIn('tags', ['draft', 'archived'])
.get()
Sorting and Pagination
Control the order and number of results:
Sort Results
// Latest posts first
const latestPosts = await Post
.orderBy('createdAt', 'desc')
.get()
// Most viewed posts
const topPosts = await Post
.orderBy('views', 'desc')
.get()
// Alphabetically by title
const alphabeticalPosts = await Post
.orderBy('title', 'asc')
.get()
Limit and Skip
// Get first 10 posts
const firstPage = await Post.limit(10).get()
// Get second page (posts 11-20)
const secondPage = await Post.skip(10).limit(10).get()
// Get top 5 most viewed posts
const top5 = await Post
.orderBy('views', 'desc')
.limit(5)
.get()
Get First Result
// Get the first matching post
const firstPublished = await Post
.where('publishedAt', '!=', null)
.orderBy('publishedAt', 'asc')
.first()
console.log(`First published post: ${firstPublished?.title}`)
Update Records
Modify existing records using the save() method:
// Find a post
const post = await Post.find(post1.id)
if (post) {
// Update properties
post.views = post.views + 1
post.title = 'Getting Started with TypeScript (Updated)'
// Save changes
await post.save()
console.log(`Updated post. Views: ${post.views}`)
console.log(`Updated at: ${post.updatedAt}`)
}
The save() method automatically updates the updatedAt timestamp.
Delete Records
Remove records using the delete() method:
// Find and delete a post
const postToDelete = await Post.find(post2.id)
if (postToDelete) {
await postToDelete.delete()
console.log('Post deleted successfully')
}
Work with Relationships
Use the hasMany() method to query related records:
// Get all posts by an author
const author = await Author.find(author.id)
const authorPosts = await author.posts().get()
console.log(`${author.name} has ${authorPosts.length} posts`)
// Get only published posts by this author
const publishedPosts = await author
.posts()
.where('publishedAt', '!=', null)
.get()
// Get the author's most popular posts
const popularPosts = await author
.posts()
.where('views', '>', 50)
.orderBy('views', 'desc')
.limit(5)
.get()
// Count author's posts
const postCount = await author.posts().count()
console.log(`Total posts: ${postCount}`)
Aggregations
Perform calculations across your data:
Count
// Count all posts
const totalPosts = await Post.count()
// Count published posts
const publishedCount = await Post
.where('publishedAt', '!=', null)
.count()
console.log(`${publishedCount} of ${totalPosts} posts are published`)
Sum
// Total views across all posts
const totalViews = await Post.sum('views')
console.log(`Total views: ${totalViews}`)
// Total views for an author's posts
const authorViews = await author.posts().sum('views')
console.log(`Author's total views: ${authorViews}`)
Average
// Average views per post
const avgViews = await Post.average('views')
console.log(`Average views per post: ${avgViews}`)
// Average for published posts only
const avgPublishedViews = await Post
.where('publishedAt', '!=', null)
.average('views')
Min and Max
// Lowest and highest view counts
const minViews = await Post.min('views')
const maxViews = await Post.max('views')
console.log(`View range: ${minViews} to ${maxViews}`)
Percentiles
// 95th percentile of post views
const p95Views = await Post.percentile('views', 95)
console.log(`95% of posts have fewer than ${p95Views} views`)
// Median views
const medianViews = await Post.percentile('views', 50)
console.log(`Median views: ${medianViews}`)
// Get all post titles
const titles = await Post.pluck('title')
console.log('All titles:', titles)
// Get all unique tags (you'll need to flatten the array)
const allTags = await Post.pluck('tags')
const uniqueTags = [...new Set(allTags.flat())]
console.log('Unique tags:', uniqueTags)
First or Create
Find an existing record or create it if it doesn’t exist:
// Find author by email, create if not found
const author = await Author.firstOrCreate(
{ email: 'john@example.com' },
{
name: 'John Smith',
bio: 'Software developer',
isActive: true
}
)
console.log(author.createdAt === author.updatedAt
? 'Created new author'
: 'Found existing author')
// When attributes aren't provided, filter is used as attributes
const defaultPost = await Post.firstOrCreate({
title: 'Welcome Post',
content: 'Welcome to our blog!',
authorId: author.id,
tags: ['welcome']
})
Custom Aggregations
For complex queries, use MongoDB’s aggregation pipeline directly:
// Group posts by author and count
const postsByAuthor = await Post.aggregate([
{
$group: {
_id: '$authorId',
count: { $sum: 1 },
totalViews: { $sum: '$views' }
}
},
{
$sort: { count: -1 }
}
])
console.log('Posts by author:', postsByAuthor)
// Get most common tags
const tagStats = await Post.aggregate([
{ $unwind: '$tags' },
{
$group: {
_id: '$tags',
count: { $sum: 1 }
}
},
{ $sort: { count: -1 } },
{ $limit: 5 }
])
console.log('Top 5 tags:', tagStats)
Complete Example
Here’s a complete working example that puts it all together:
import { BaseModel } from 'esix'
// Define models
class Author extends BaseModel {
public name = ''
public email = ''
public bio = ''
public isActive = true
posts() {
return this.hasMany(Post, 'authorId')
}
}
class Post extends BaseModel {
public title = ''
public content = ''
public authorId = ''
public tags: string[] = []
public views = 0
public publishedAt: Date | null = null
}
async function main() {
// Create an author
const author = await Author.firstOrCreate(
{ email: 'jane@example.com' },
{
name: 'Jane Doe',
bio: 'Technology writer and blogger',
isActive: true
}
)
console.log(`Author: ${author.name} (${author.id})`)
// Create posts
const post1 = await Post.create({
title: 'Getting Started with TypeScript',
content: 'TypeScript is a powerful superset of JavaScript that adds static typing...',
authorId: author.id,
tags: ['typescript', 'programming', 'tutorial'],
views: 150,
publishedAt: new Date()
})
const post2 = await Post.create({
title: 'MongoDB Best Practices',
content: 'Learn how to structure your MongoDB databases for optimal performance...',
authorId: author.id,
tags: ['mongodb', 'database', 'tutorial'],
views: 230,
publishedAt: new Date()
})
const post3 = await Post.create({
title: 'Advanced TypeScript Patterns',
content: 'Dive deep into advanced TypeScript patterns and techniques...',
authorId: author.id,
tags: ['typescript', 'advanced', 'patterns'],
views: 89,
publishedAt: null // Draft post
})
// Query published posts
console.log('\n=== Published Posts ===')
const publishedPosts = await Post
.where('publishedAt', '!=', null)
.orderBy('views', 'desc')
.get()
publishedPosts.forEach(post => {
console.log(`- ${post.title} (${post.views} views)`)
})
// Get author's stats
console.log('\n=== Author Stats ===')
const totalPosts = await author.posts().count()
const totalViews = await author.posts().sum('views')
const avgViews = await author.posts().average('views')
console.log(`Total posts: ${totalPosts}`)
console.log(`Total views: ${totalViews}`)
console.log(`Average views: ${avgViews.toFixed(1)}`)
// Find most popular post
console.log('\n=== Most Popular Post ===')
const topPost = await author
.posts()
.where('publishedAt', '!=', null)
.orderBy('views', 'desc')
.first()
if (topPost) {
console.log(`${topPost.title} - ${topPost.views} views`)
}
// Update a post
console.log('\n=== Updating Post ===')
const postToUpdate = await Post.find(post1.id)
if (postToUpdate) {
postToUpdate.views += 25
await postToUpdate.save()
console.log(`Updated views: ${postToUpdate.views}`)
}
// Get tag statistics
console.log('\n=== Tag Statistics ===')
const tagStats = await Post.aggregate([
{ $match: { publishedAt: { $ne: null } } },
{ $unwind: '$tags' },
{
$group: {
_id: '$tags',
count: { $sum: 1 },
totalViews: { $sum: '$views' }
}
},
{ $sort: { totalViews: -1 } }
])
tagStats.forEach((stat: any) => {
console.log(`${stat._id}: ${stat.count} posts, ${stat.totalViews} views`)
})
}
main().catch(console.error)
Run it:
Next Steps
Now that you’ve built your first Esix application, explore these topics to learn more: