Quick Start Guide
Build your first OData API with Express.js in 5 minutes! This guide uses the ExpressRouter for a complete, production-ready solution.
What You'll Build
A fully functional REST API with OData v4 query capabilities for a simple blog system with Users and Posts.
Features you'll get:
- ✅ Full OData query support ($filter, $select, $expand, $orderby, $top, $skip)
- ✅ Automatic metadata endpoint
- ✅ Type-safe models with decorators
- ✅ Relationship support (one-to-many)
- ✅ Connection pooling for performance
Step 1: Create a New Project
# Create project directory
mkdir my-odata-api
cd my-odata-api
# Initialize npm project
npm init -y
# Install dependencies
npm install @phrasecode/odata express sqlite3
npm install --save-dev typescript @types/express @types/node ts-node nodemon
# Initialize TypeScript
npx tsc --init
Step 2: Configure TypeScript
Update your tsconfig.json to enable decorators:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2020",
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Step 3: Define Your Models
Create src/models/user.ts:
import { Model, Table, Column, DataTypes, HasMany } from '@phrasecode/odata';
import { Post } from './post';
@Table({ tableName: 'users' })
export class User extends Model<User> {
@Column({
dataType: DataTypes.INTEGER,
isPrimaryKey: true,
isAutoIncrement: true,
})
id!: number;
@Column({
dataType: DataTypes.STRING,
isNullable: false,
})
name!: string;
@Column({
dataType: DataTypes.STRING,
isNullable: false,
})
email!: string;
@Column({
dataType: DataTypes.INTEGER,
isNullable: true,
})
age!: number;
// Define one-to-many relationship
@HasMany(() => Post, {
relation: [
{
foreignKey: 'userId',
sourceKey: 'id',
},
],
})
posts!: Post[];
}
Create src/models/post.ts:
import { Model, Table, Column, DataTypes, BelongsTo } from '@phrasecode/odata';
import { User } from './user';
@Table({ tableName: 'posts' })
export class Post extends Model<Post> {
@Column({
dataType: DataTypes.INTEGER,
isPrimaryKey: true,
isAutoIncrement: true,
})
id!: number;
@Column({
dataType: DataTypes.STRING,
isNullable: false,
})
title!: string;
@Column({
dataType: DataTypes.TEXT,
isNullable: true,
})
content!: string;
@Column({
dataType: DataTypes.INTEGER,
isNullable: false,
})
userId!: number;
@Column({
dataType: DataTypes.DATE,
isNullable: true,
})
publishedAt!: Date;
// Define many-to-one relationship
@BelongsTo(() => User, {
relation: [
{
foreignKey: 'id',
sourceKey: 'userId',
},
],
})
user!: User;
}
Step 4: Create Controllers
Create src/controllers/user.controller.ts:
import { ODataControler, QueryParser } from '@phrasecode/odata';
import { User } from '../models/user';
export class UserController extends ODataControler {
constructor() {
super({
model: User,
allowedMethod: ['get'],
});
}
// Optional: Override to add custom logic
public async get(query: QueryParser) {
// Add custom business logic here if needed
const result = await this.queryable<User>(query);
return result;
}
}
Create src/controllers/post.controller.ts:
import { ODataControler } from '@phrasecode/odata';
import { Post } from '../models/post';
export class PostController extends ODataControler {
constructor() {
super({
model: Post,
allowedMethod: ['get'],
});
}
}
Step 5: Set Up Express Server
Create src/server.ts:
import express from 'express';
import { DataSource, ExpressRouter } from '@phrasecode/odata';
import { User } from './models/user';
import { Post } from './models/post';
import { UserController } from './controllers/user.controller';
import { PostController } from './controllers/post.controller';
const app = express();
const PORT = 3000;
// Parse JSON request bodies
app.use(express.json());
// Create DataSource with connection pooling
const dataSource = new DataSource({
dialect: 'sqlite',
storage: './database.sqlite',
models: [User, Post],
pool: {
max: 5,
min: 0,
idle: 10000,
},
});
// Initialize controllers
const userController = new UserController();
const postController = new PostController();
// Set up ExpressRouter with controllers
new ExpressRouter(app, {
controllers: [userController, postController],
dataSource,
logger: {
enabled: true,
logLevel: 'INFO',
advancedOptions: {
logDbExecutionTime: true,
},
},
});
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
// Start server
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
});
Step 6: Add NPM Scripts
Update your package.json:
{
"name": "my-odata-api",
"version": "1.0.0",
"scripts": {
"dev": "nodemon --exec ts-node src/server.ts",
"build": "tsc",
"start": "node dist/server.js"
},
"dependencies": {
"@phrasecode/odata": "^0.2.1",
"express": "^4.18.0",
"sqlite3": "^5.1.0"
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/node": "^20.0.0",
"nodemon": "^3.0.0",
"ts-node": "^10.9.0",
"typescript": "^5.1.0"
}
}
Step 7: Run Your API
npm run dev
You should see:
🚀 Server running on http://localhost:3000
📊 Metadata: http://localhost:3000/$metadata
👥 Users: http://localhost:3000/User
📝 Posts: http://localhost:3000/Post
Step 8: Test Your API
Get Metadata
curl http://localhost:3000/$metadata
Returns OData v4 compliant XML metadata describing your entities.
Get All Users
curl http://localhost:3000/User
Select Specific Fields
curl "http://localhost:3000/User?\$select=name,email"
Filter Users
# Users older than 25
curl "http://localhost:3000/User?\$filter=age gt 25"
# Users with specific email
curl "http://localhost:3000/User?\$filter=email eq 'john@example.com'"
Expand Relationships
# Get users with their posts
curl "http://localhost:3000/User?\$expand=posts"
# Get posts with user information
curl "http://localhost:3000/Post?\$expand=user"
Combine Multiple Options
# Get users over 25, select name and email, include posts, order by name
curl "http://localhost:3000/User?\$filter=age gt 25&\$select=name,email&\$expand=posts&\$orderby=name asc"
Pagination
# Get first 10 users
curl "http://localhost:3000/User?\$top=10"
# Skip first 10, get next 10
curl "http://localhost:3000/User?\$top=10&\$skip=10"
Count Results
# Get total count of users
curl "http://localhost:3000/User?\$count=true"
Understanding the Response
All responses follow this structure:
{
"data": [
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"age": 30
}
],
"meta": {
"count": 1,
"totalExecutionTime": "45ms"
}
}
- data: Array of results
- meta.count: Total number of records (when
$count=true) - meta.totalExecutionTime: Query execution time
Project Structure
Your final project structure should look like:
my-odata-api/
├── src/
│ ├── models/
│ │ ├── user.ts
│ │ └── post.ts
│ ├── controllers/
│ │ ├── user.controller.ts
│ │ └── post.controller.ts
│ └── server.ts
├── package.json
├── tsconfig.json
What Just Happened?
Let's break down what the ExpressRouter did for you automatically:
1. Automatic Route Registration
new ExpressRouter(app, {
controllers: [userController, postController],
dataSource,
});
This single line:
- ✅ Registered
/Userendpoint for the User model - ✅ Registered
/Postendpoint for the Post model - ✅ Set up
/$metadataendpoint automatically - ✅ Configured OData query parsing for all endpoints
- ✅ Added error handling and logging
2. OData Query Support
Each endpoint automatically supports:
- $select - Choose which fields to return
- $filter - Filter results with complex conditions
- $expand - Include related entities
- $orderby - Sort results
- $top - Limit number of results
- $skip - Skip results for pagination
- $count - Get total count
3. Metadata Endpoint
The /$metadata endpoint provides:
- Complete schema information
- Entity definitions
- Property types
- Relationship mappings
- OData v4 compliant XML
Key Concepts
Controllers
Controllers define which HTTP methods are allowed and can add custom business logic:
export class UserController extends ODataControler {
constructor() {
super({
model: User, // The model this controller handles
allowedMethod: ['get'], // Only allow GET requests
});
}
// Override to add custom logic
public async get(query: QueryParser) {
// Add authentication, validation, etc.
const result = await this.queryable<User>(query);
return result;
}
}
DataSource
The DataSource manages database connections and model registration:
const dataSource = new DataSource({
dialect: 'sqlite', // Database type
database: 'my-database',
models: [User, Post], // Register all models
pool: {
// Connection pooling for performance
max: 5,
min: 0,
idle: 10000,
},
});
Models
Models define your data structure using decorators:
@Table({ tableName: 'users' })
export class User extends Model<User> {
@Column({
dataType: DataTypes.INTEGER,
isPrimaryKey: true,
isAutoIncrement: true,
})
id!: number;
@HasMany(() => Post, {
relation: [
{
foreignKey: 'id',
sourceKey: 'userId',
},
],
})
posts!: Post[];
}
Adding Authentication
Add authentication middleware to your controller:
export class UserController extends ODataControler {
constructor() {
super({
model: User,
allowedMethod: ['get'],
});
}
public async get(query: QueryParser) {
// Add authentication check
const token = query.getHeaders()?.authorization;
if (!token) {
throw new Error('Unauthorized');
}
// Verify token...
const result = await this.queryable<User>(query);
return result;
}
}
Summary
In this quick start, you:
- ✅ Created a TypeScript project with OData support
- ✅ Defined models with decorators and relationships
- ✅ Set up controllers for business logic
- ✅ Configured ExpressRouter for automatic endpoint registration
- ✅ Tested OData queries ($filter, $select, $expand, etc.)
- ✅ Learned about metadata endpoints
- ✅ Understood the response structure
Time to build: ~5 minutes Lines of code: ~150 lines Features gained: Full OData v4 API with relationships, filtering, pagination, and more!