OData V4 Server for Node.js

0.3.0 · abandoned · verified Wed Apr 22

odata4-server is an abandoned Node.js library for creating OData V4 compliant servers, originally forked from the equally unmaintained `jaystack/odata-v4-server`. It provides a decorator-driven approach to define OData controllers and expose service metadata (`$metadata`), supporting a wide range of OData query language features like filtering (`$filter`), sorting (`$orderby`), paging (`$skip`, `$top`), projection (`$select`), and expansion (`$expand`). It can operate as a standalone server, an Express router, or a Node.js stream. The package is currently at version 0.3.0, with its last update more than seven years ago (as of April 2026), indicating it is no longer actively maintained. Due to its abandoned status, it's not recommended for new projects or production use.

Common errors

Warnings

Install

Imports

Quickstart

This quickstart sets up a basic OData V4 server with two entity sets, 'Products' and 'Categories', demonstrating CRUD operations and OData query capabilities using decorators. It includes mock data and logs requests to the console, listening on port 3000 by default. Requires TypeScript compilation with decorator support.

import { createFilter, ODataController, ODataServer, ODataQuery } from 'odata4-server';
import * as odata from 'odata4-server';

// Mock data for demonstration
interface Product { _id: string; name: string; categoryId: string; }
interface Category { _id: string; name: string; }

const products: Product[] = [
  { _id: '1', name: 'Apples', categoryId: 'cat1' },
  { _id: '2', name: 'Bananas', categoryId: 'cat1' },
  { _id: '3', name: 'Carrots', categoryId: 'cat2' }
];

const categories: Category[] = [
  { _id: 'cat1', name: 'Fruits' },
  { _id: 'cat2', name: 'Vegetables' }
];

// Simple unique ID generator (replace with proper DB IDs in real app)
let nextId = 10;
const generateId = () => (nextId++).toString();

@odata.controller(products, 'Products')
export class ProductsController extends ODataController{
    @odata.GET
    find(@odata.filter filter: ODataQuery) {
        console.log('GET /Products', filter);
        if (filter) return products.filter(createFilter(filter));
        return products;
    }

    @odata.GET
    findOne(@odata.key key: string) {
        console.log(`GET /Products(${key})`);
        return products.filter(product => product._id === key)[0];
    }

    @odata.POST
    insert(@odata.body product: any) {
        console.log('POST /Products', product);
        product._id = generateId();
        products.push(product);
        return product;
    }

    @odata.PATCH
    update(@odata.key key: string, @odata.body delta: any) {
        console.log(`PATCH /Products(${key})`, delta);
        let product = products.filter(product => product._id === key)[0];
        if (product) {
            for (let prop in delta) {
                if (Object.prototype.hasOwnProperty.call(delta, prop)) {
                    (product as any)[prop] = delta[prop];
                }
            }
        }
    }

    @odata.DELETE
    remove(@odata.key key: string) {
        console.log(`DELETE /Products(${key})`);
        const index = products.findIndex(product => product._id === key);
        if (index > -1) {
            products.splice(index, 1);
        }
    }
}

@odata.controller(categories, 'Categories')
export class CategoriesController extends ODataController{
    @odata.GET
    find(@odata.filter filter:ODataQuery) {
        console.log('GET /Categories', filter);
        if (filter) return categories.filter(createFilter(filter));
        return categories;
    }

    @odata.GET
    findOne(@odata.key key:string) {
        console.log(`GET /Categories(${key})`);
        return categories.filter(category => category._id === key)[0];
    }

    @odata.POST
    insert(@odata.body category:any) {
        console.log('POST /Categories', category);
        category._id = generateId();
        categories.push(category);
        return category;
    }

    @odata.PATCH
    update(@odata.key key:string, @odata.body delta:any) {
        console.log(`PATCH /Categories(${key})`, delta);
        let category = categories.filter(category => category._id === key)[0];
        if (category) {
            for (let prop in delta) {
                if (Object.prototype.hasOwnProperty.call(delta, prop)) {
                    (category as any)[prop] = delta[prop];
                }
            }
        }
    }

    @odata.DELETE
    remove(@odata.key key:string) {
        console.log(`DELETE /Categories(${key})`);
        const index = categories.findIndex(category => category._id === key);
        if (index > -1) {
            categories.splice(index, 1);
        }
    }
}

@odata.cors
@odata.controller(ProductsController, true)
@odata.controller(CategoriesController, true)
export class NorthwindODataServer extends ODataServer{}

const PORT = process.env.PORT || 3000;
NorthwindODataServer.create('/odata', PORT);
console.log(`OData server listening on http://localhost:${PORT}/odata`);
console.log(`Access metadata at http://localhost:${PORT}/odata/$metadata`);
console.log(`Try: http://localhost:${PORT}/odata/Products?$filter=name eq 'Apples'`);
console.log(`Try: http://localhost:${PORT}/odata/Categories('cat1')`);

view raw JSON →