Skip to main content

Next.js Getting Started Guide

Complete guide to integrating OData APIs with Next.js using the OpenRouter solution.

Overview

The OpenRouter provides a framework-agnostic solution perfect for Next.js applications. It gives you:

  • ✅ Full control over route registration and paths
  • ✅ Compatibility with both App Router and Pages Router
  • ✅ Direct model querying without controllers
  • ✅ Flexible middleware integration
  • ✅ Support for custom authentication and authorization

Prerequisites

  • Node.js 16+ installed
  • Next.js 13+ project (App Router or Pages Router)
  • Basic knowledge of Next.js API routes
  • Database server running (PostgreSQL, MySQL, SQLite, etc.)

Installation

1. Install the package

npm install @phrasecode/odata

2. Install your database driver

Choose one based on your database:

# PostgreSQL
npm install pg pg-hstore

# MySQL
npm install mysql2

# SQLite
npm install sqlite3

See the Installation Guide for details.

Step-by-Step Setup

Step 1: Define Your Models

Create your data models using decorators.

lib/models/user.ts:

import { Model, Table, Column, DataTypes, HasMany } from '@phrasecode/odata';
import type { Order } from './order';

@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, isUnique: true })
email: string;

@Column({ dataType: DataTypes.INTEGER })
age: number;

@HasMany(() => require('./order').Order, {
relation: [{ foreignKey: 'userId', sourceKey: 'id' }],
})
orders: Order[];
}

lib/models/order.ts:

import { Model, Table, Column, DataTypes, BelongsTo } from '@phrasecode/odata';
import type { User } from './user';

@Table({ tableName: 'orders' })
export class Order extends Model<Order> {
@Column({
dataType: DataTypes.INTEGER,
isPrimaryKey: true,
isAutoIncrement: true,
})
id: number;

@Column({ dataType: DataTypes.INTEGER })
userId: number;

@Column({ dataType: DataTypes.DECIMAL })
total: number;

@BelongsTo(() => require('./user').User, {
relation: [{ foreignKey: 'id', sourceKey: 'userId' }],
})
user: User;
}

Step 2: Create DataSource Singleton

IMPORTANT: In Next.js, create a singleton DataSource to avoid multiple database connections.

lib/datasource.ts:

import { DataSource } from '@phrasecode/odata';
import { User } from './models/user';
import { Order } from './models/order';

let dataSource: DataSource | null = null;

export function getDataSource(): DataSource {
if (!dataSource) {
dataSource = new DataSource({
dialect: 'postgres',
database: process.env.DB_NAME!,
username: process.env.DB_USER!,
password: process.env.DB_PASSWORD!,
host: process.env.DB_HOST!,
port: parseInt(process.env.DB_PORT || '5432'),

// Connection pooling is CRITICAL for Next.js
pool: {
max: 10,
min: 2,
idle: 10000,
acquire: 30000,
},

schema: 'public',
ssl: process.env.NODE_ENV === 'production',

models: [User, Order],
});
}

return dataSource;
}

Step 3: Create OpenRouter Instance

lib/odata-router.ts:

import { OpenRouter } from '@phrasecode/odata';
import { getDataSource } from './datasource';

let router: OpenRouter | null = null;

export function getODataRouter(): OpenRouter {
if (!router) {
router = new OpenRouter({
dataSource: getDataSource(),
logger: {
enabled: true,
logLevel: process.env.NODE_ENV === 'production' ? 'ERROR' : 'INFO',
format: 'JSON',
},
});
}

return router;
}

App Router Implementation (Next.js 13+)

Step 4A: Create API Routes (App Router)

User Endpoint

app/api/odata/user/route.ts:

import { NextRequest, NextResponse } from 'next/server';
import { getODataRouter } from '@/lib/odata-router';
import { User } from '@/lib/models/user';

export async function GET(request: NextRequest) {
try {
const router = getODataRouter();
const path = request.url; // /api/User?$select=name&$filter=age gt 18

// Use queryable() to execute OData query
const result = router.queryable(User)(path)

return NextResponse.json(result);
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: error.statusCode || 500 });
}
}

Order Endpoint

app/api/odata/order/route.ts:

import { NextRequest, NextResponse } from 'next/server';
import { getODataRouter } from '@/lib/odata-router';
import { Order } from '@/lib/models/order';

export async function GET(request: NextRequest) {
try {
const router = getODataRouter();
const path = request.url; // /api/Order?$select=name&$filter=age gt 18

const result = router.queryable(Order)(path);

return NextResponse.json(result);
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: error.statusCode || 500 });
}
}

Metadata Endpoint

app/api/odata/$metadata/route.ts:

import { NextResponse } from 'next/server';
import { getDataSource } from '@/lib/datasource';

export async function GET() {
try {
const dataSource = getDataSource();
const metadata = dataSource.getMetadata();

return new NextResponse.json(metadata);
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

Pages Router Implementation (Next.js 12)

Step 4B: Create API Routes (Pages Router)

User Endpoint

pages/api/odata/user.ts:

import type { NextApiRequest, NextApiResponse } from 'next';
import { getODataRouter } from '@/lib/odata-router';
import { User } from '@/lib/models/user';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}

try {
const router = getODataRouter();

const path = req.url;

const result = router.queryable(User)(path);

res.status(200).json(result);
} catch (error: any) {
res.status(error.statusCode || 500).json({ error: error.message });
}
}

Metadata Endpoint

pages/api/odata/$metadata.ts:

import type { NextApiRequest, NextApiResponse } from 'next';
import { getDataSource } from '@/lib/datasource';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}

try {
const dataSource = getDataSource();
const metadata = dataSource.getMetadata();

res.setHeader('Content-Type', 'application/xml');
res.status(200).send(metadata);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
}

Environment Variables

Create a .env.local file:

# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=mydb
DB_USER=user
DB_PASSWORD=password

# Environment
NODE_ENV=development

Example Queries

Once your API routes are set up, you can query them:

# Get all users
curl http://localhost:3000/api/odata/user

# Select specific fields
curl http://localhost:3000/api/odata/user?$select=name,email

# Filter by condition
curl http://localhost:3000/api/odata/user?$filter=age gt 18

# Expand relationships
curl http://localhost:3000/api/odata/user?$expand=orders

# Metadata
curl http://localhost:3000/api/odata/$metadata

Adding Authentication

Middleware Approach (App Router)

middleware.ts:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
// Only apply to OData routes
if (request.nextUrl.pathname.startsWith('/api/odata')) {
const token = request.headers.get('authorization');

if (!token || !isValidToken(token)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
}

return NextResponse.next();
}

function isValidToken(token: string): boolean {
// Implement your token validation logic
return token === 'Bearer valid-token';
}

export const config = {
matcher: '/api/odata/:path*',
};

Per-Route Authentication

app/api/odata/user/route.ts:

import { NextRequest, NextResponse } from 'next/server';
import { getODataRouter } from '@/lib/odata-router';
import { User } from '@/lib/models/user';

async function authenticate(request: NextRequest): Promise<boolean> {
const token = request.headers.get('authorization');
// Implement your authentication logic
return token === 'Bearer valid-token';
}

export async function GET(request: NextRequest) {
// Check authentication
if (!(await authenticate(request))) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

try {
const router = getODataRouter();
const queryString = request.nextUrl.searchParams.toString();
const handler = router.queryable(User);
const result = await handler(queryString);

return NextResponse.json(result);
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: error.statusCode || 500 });
}
}

Custom Query Logic

Add custom logic before executing queries:

export async function GET(request: NextRequest) {
try {
const router = getODataRouter();
let queryString = request.nextUrl.searchParams.toString();

// Example: Force a filter for active users only
const params = new URLSearchParams(queryString);
const existingFilter = params.get('$filter');

if (existingFilter) {
params.set('$filter', `(${existingFilter}) and status eq 'active'`);
} else {
params.set('$filter', "status eq 'active'");
}

// Example: Limit maximum results
const top = params.get('$top');
if (!top || parseInt(top) > 100) {
params.set('$top', '100');
}

queryString = params.toString();

const handler = router.queryable(User);
const result = await handler(queryString);

return NextResponse.json(result);
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: error.statusCode || 500 });
}
}