NIXX/DEVv1.14.0
ArticlesFavorites
Sign In
Sign In
Articles

Welcome to our blog

A curated collection of insightful articles, practical guides, and expert tips designed to simplify your workflow

Cover image for: How to Schedule API Calls Without Zapier or n8n
November 7, 20257 MIN READ min readBy ℵi✗✗

How to Schedule API Calls Without Zapier or n8n

Schedule and monitor API calls with a self-hosted dashboard built for developers.

productivityautomationapi schedulern8nzapier
ℵi✗✗

ℵi✗✗

Full-Stack Developer

Passionate about building tools and sharing knowledge with the developer community.

Was this helpful?

Popular Posts

  • NixOS vs. Arch Linux: Which One Belongs in Your Dev Setup?

    NixOS vs. Arch Linux: Which One Belongs in Your Dev Setup?

    5 MIN READ min read

  • How to Enable HTTPS on Localhost in Under 2 Minutes

    How to Enable HTTPS on Localhost in Under 2 Minutes

    3 MIN READ min read

  • Migrating from Create React App (CRA) to Vite: A Step-by-Step Guide

    Migrating from Create React App (CRA) to Vite: A Step-by-Step Guide

    4 MIN READ min read

  • Array Destructuring in PHP: A Practical Guide for Modern Developers

    Array Destructuring in PHP: A Practical Guide for Modern Developers

    5 MIN READ min read

Recommended Products

  • Apple iPad (7th Gen)

    Apple iPad (7th Gen)

    4.3
  • Fitbit Versa 4

    Fitbit Versa 4

    4.3
  • JBL Flip 6

    JBL Flip 6

    4.8
  • Dell 24 Monitor — SE2425HM Full HD

    Dell 24 Monitor — SE2425HM Full HD

    4.7

May contain affiliate links

Topics

webdev33productivity16cybersecurity12javascript11automation9guide8react7typescript7php6tutorial6freelancing5github actions5privacy5how to4Node.js4
+111 more topics →
🇺🇸USD ACCOUNTOpen a free US-based USD accountReceive & save in USD — powered by ClevaSponsoredInterserver Hosting#1 VALUEAffordable, reliable hosting from $2.50/mo99.9% uptimeSponsored

Scheduled API calls are a recurring need in most development projects: syncing data on a timer, refreshing tokens, polling an external service, clearing caches. The typical solution is a cron job on a VPS or a workflow in a third-party automation tool, both of which work until they do not. Cron jobs on remote servers are easy to lose track of, and third-party tools add cost and opacity for what is fundamentally a simple requirement.

This guide builds a self-hosted API scheduler: a Next.js application with a Prisma-backed database, a node-cron worker that executes registered endpoints on schedule, structured logging for every execution, and NextAuth.js for authentication. The result is a dashboard where you can add an endpoint, set a cron expression, and see every call and its outcome.

What this covers:

  • Project setup and dependencies

  • Prisma models for endpoints and logs

  • The scheduler worker with error handling and logging

  • NextAuth.js credentials authentication

  • Dashboard routes for managing and monitoring endpoints

  • Deployment considerations


Why Build Your Own

Third-party automation tools like Zapier and n8n are well-suited for multi-step workflows with many integrations. For the simpler case of calling a set of API endpoints on a schedule, they introduce unnecessary overhead: monthly costs for running a handful of background jobs, limited visibility into what happened when a call fails, and no control over retry logic or timeout behavior.

A custom scheduler built on a standard Next.js stack gives direct control over all of these and runs on any server or serverless platform where Node.js is available.


Step 1: Project Setup

Create the project and install dependencies:

npx create-next-app@latest api-scheduler --typescript
cd api-scheduler
npm install axios node-cron @prisma/client next-auth bcryptjs
npm install --save-dev prisma @types/node-cron @types/bcryptjs

Initialize Prisma:

npx prisma init

Create the environment file:

DATABASE_URL="postgresql://user:password@localhost:5432/api_scheduler"
NEXTAUTH_SECRET="replace-with-a-long-random-string"
NEXTAUTH_URL="http://localhost:3000"

Generate the NEXTAUTH_SECRET with:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Step 2: Define the Database Models

Update prisma/schema.prisma with the models for endpoints, logs, and users:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Endpoint {
  id        String   @id @default(cuid())
  name      String
  url       String
  method    String   @default("GET")
  headers   Json?
  body      Json?
  schedule  String   // cron expression, e.g. "0 */6 * * *"
  enabled   Boolean  @default(true)
  lastRun   DateTime?
  createdAt DateTime @default(now())
  logs      Log[]
}

model Log {
  id         String   @id @default(cuid())
  endpointId String
  endpoint   Endpoint @relation(fields: [endpointId], references: [id])
  status     Int
  duration   Int      // milliseconds
  error      String?
  createdAt  DateTime @default(now())
}

model User {
  id       String @id @default(cuid())
  email    String @unique
  password String
}

Run the migration:

npx prisma migrate dev --name init

Step 3: The Scheduler Worker

Create src/lib/scheduler.ts. This worker loads all enabled endpoints, registers a cron job for each, and logs every execution including failures:

import cron from 'node-cron';
import axios from 'axios';
import { db } from './db';

const activeJobs = new Map<string, cron.ScheduledTask>();

export async function startScheduler(): Promise<void> {
    const endpoints = await db.endpoint.findMany({
        where: { enabled: true },
    });

    for (const endpoint of endpoints) {
        if (!cron.validate(endpoint.schedule)) {
            console.warn(`Invalid cron expression for endpoint ${endpoint.id}: ${endpoint.schedule}`);
            continue;
        }

        const task = cron.schedule(endpoint.schedule, async () => {
            await runEndpoint(endpoint.id);
        });

        activeJobs.set(endpoint.id, task);
    }

    console.log(`Scheduler started with ${endpoints.length} active endpoints`);
}

export async function runEndpoint(endpointId: string): Promise<void> {
    const endpoint = await db.endpoint.findUnique({ where: { id: endpointId } });
    if (!endpoint) return;

    const start = Date.now();

    try {
        const res = await axios({
            method: endpoint.method as string,
            url: endpoint.url,
            headers: (endpoint.headers as Record<string, string>) ?? {},
            data: endpoint.body ?? undefined,
            timeout: 30_000,
        });

        await db.log.create({
            data: {
                endpointId: endpoint.id,
                status: res.status,
                duration: Date.now() - start,
            },
        });

        await db.endpoint.update({
            where: { id: endpoint.id },
            data: { lastRun: new Date() },
        });
    } catch (err: unknown) {
        const status =
            axios.isAxiosError(err) ? (err.response?.status ?? 500) : 500;
        const message =
            err instanceof Error ? err.message : 'Unknown error';

        await db.log.create({
            data: {
                endpointId: endpoint.id,
                status,
                error: message,
                duration: Date.now() - start,
            },
        });
    }
}

export function stopJob(endpointId: string): void {
    const job = activeJobs.get(endpointId);
    if (job) {
        job.stop();
        activeJobs.delete(endpointId);
    }
}

A few implementation details worth noting:

cron.validate() checks the cron expression before registering a job, which prevents the scheduler from crashing on a malformed schedule.

The error handler uses axios.isAxiosError() for type-safe error narrowing rather than casting to any. The logged status code comes from the actual HTTP response when one was received, defaulting to 500 when the request did not reach the server at all (network error, timeout).

The lastRun timestamp is updated on success so the dashboard can show when each endpoint last ran without querying the logs table.


Step 4: Database Client Singleton

Create src/lib/db.ts to prevent multiple Prisma client instances in development (which causes connection pool exhaustion with Next.js hot reloading):

import { PrismaClient } from '@prisma/client';

const globalForPrisma = globalThis as unknown as {
    prisma: PrismaClient | undefined;
};

export const db =
    globalForPrisma.prisma ??
    new PrismaClient({ log: ['error'] });

if (process.env.NODE_ENV !== 'production') {
    globalForPrisma.prisma = db;
}

Step 5: Authentication with NextAuth.js

Create src/app/api/auth/[...nextauth]/route.ts:

import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import bcrypt from 'bcryptjs';
import { db } from '@/lib/db';

const handler = NextAuth({
    providers: [
        CredentialsProvider({
            name: 'Credentials',
            credentials: {
                email: { label: 'Email', type: 'email' },
                password: { label: 'Password', type: 'password' },
            },
            async authorize(credentials) {
                if (!credentials?.email || !credentials.password) return null;

                const user = await db.user.findUnique({
                    where: { email: credentials.email },
                });

                if (!user) return null;

                const valid = await bcrypt.compare(credentials.password, user.password);
                if (!valid) return null;

                return { id: user.id, email: user.email };
            },
        }),
    ],
    session: { strategy: 'jwt' },
    secret: process.env.NEXTAUTH_SECRET,
});

export { handler as GET, handler as POST };

Seed an initial user:

// scripts/seed.ts
import bcrypt from 'bcryptjs';
import { db } from '../src/lib/db';

async function main() {
    const hashed = await bcrypt.hash('yourpassword', 12);
    await db.user.create({
        data: { email: '[email protected]', password: hashed },
    });
}

main();

Step 6: API Routes for Endpoints

Create src/app/api/endpoints/route.ts for listing and creating endpoints:

import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '../auth/[...nextauth]/route';
import { db } from '@/lib/db';
import { runEndpoint } from '@/lib/scheduler';

export async function GET() {
    const session = await getServerSession(authOptions);
    if (!session) return NextResponse.json({ error: 'Unauthorised' }, { status: 401 });

    const endpoints = await db.endpoint.findMany({
        orderBy: { createdAt: 'desc' },
        include: { logs: { take: 1, orderBy: { createdAt: 'desc' } } },
    });

    return NextResponse.json(endpoints);
}

export async function POST(req: Request) {
    const session = await getServerSession(authOptions);
    if (!session) return NextResponse.json({ error: 'Unauthorised' }, { status: 401 });

    const body = await req.json();

    const endpoint = await db.endpoint.create({
        data: {
            name: body.name,
            url: body.url,
            method: body.method ?? 'GET',
            headers: body.headers ?? null,
            body: body.body ?? null,
            schedule: body.schedule,
        },
    });

    return NextResponse.json(endpoint, { status: 201 });
}

Add a manual trigger endpoint at src/app/api/endpoints/[id]/trigger/route.ts:

import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '../../../auth/[...nextauth]/route';
import { runEndpoint } from '@/lib/scheduler';

export async function POST(
    _req: Request,
    { params }: { params: { id: string } }
) {
    const session = await getServerSession(authOptions);
    if (!session) return NextResponse.json({ error: 'Unauthorised' }, { status: 401 });

    await runEndpoint(params.id);
    return NextResponse.json({ triggered: true });
}

Step 7: Starting the Scheduler

In a Next.js App Router project, the scheduler needs to start when the server initializes. Create a custom server or use a startup route. For a simple approach in src/app/api/scheduler/start/route.ts called on server startup:

import { startScheduler } from '@/lib/scheduler';

let started = false;

export async function GET() {
    if (!started) {
        await startScheduler();
        started = true;
    }
    return new Response('Scheduler running', { status: 200 });
}

For production, a more robust approach is to use a dedicated background process (PM2 worker, a separate Node.js process) that runs the scheduler independently of the web server.


Deployment

The application deploys to any platform that supports Node.js and a PostgreSQL database.

For Vercel: the API routes and dashboard deploy normally, but the cron scheduler cannot run as a persistent process on Vercel's serverless functions. Use Vercel Cron Jobs to call a trigger endpoint on schedule, which runs each registered endpoint.

For a VPS or Fly.io: run the Next.js server with PM2 and let the scheduler run as a persistent background process within the same Node.js instance or as a separate worker.

For Railway or Render: the web service can run the scheduler as part of the same process, and the managed PostgreSQL add-on handles the database.


Key Takeaways

  • node-cron handles the scheduling; Prisma handles persistence; Axios handles the HTTP calls. The architecture is minimal and each part is independently replaceable.

  • Validate cron expressions before registering jobs to prevent the scheduler from crashing on malformed input.

  • Use axios.isAxiosError() for type-safe error handling rather than casting to any.

  • Log every execution including failures, with the HTTP status code and duration. This is the visibility that makes the scheduler useful in practice.

  • Authentication on all API routes is essential since the dashboard exposes the ability to trigger arbitrary HTTP requests.

  • The correct deployment approach depends on the platform. Serverless platforms require a different trigger mechanism than persistent server deployments.


Conclusion

A self-hosted API scheduler built on this stack is a few hundred lines of TypeScript and provides complete visibility and control over scheduled API calls. The logs table answers the questions that matter in practice: did the call succeed, how long did it take, and if it failed, what was the error.

The architecture scales in the expected directions: additional endpoint types, retry logic with exponential backoff, alert notifications on failure, and role-based access control are all straightforward additions to the same foundation.


Building something similar or extending this with a specific feature? Share what you are working on in the comments.

Topics
productivityautomationapi schedulern8nzapier
Interserver Hosting#1 VALUEAffordable, reliable hosting from $2.50/mo99.9% uptimeSponsored

Discussion

Join the discussion

Sign in to share your thoughts and engage with the community.

Sign In
Loading comments…

Continue Reading

More Articles

View all
Cover image for: AI for DevOps: Tools That Are Already Changing the Game
Jun 17, 20256 MIN READ min read

AI for DevOps: Tools That Are Already Changing the Game

How artificial intelligence is transforming CI/CD pipelines, monitoring, and incident response—today.

Cover image for: Best Web Hosting of 2026 (Honest Picks From Real-World Use)
Jan 1, 20267 MIN READ min read

Best Web Hosting of 2026 (Honest Picks From Real-World Use)

Choosing the right web hosting in 2026 isn't just about price. A breakdown of the best providers, focusing on reliability, performance, and support.

Cover image for: Embedding Cybersecurity in Development: Best Practices for 2025
Jul 1, 20257 MIN READ min read

Embedding Cybersecurity in Development: Best Practices for 2025

A developer-focused guide to integrating security into your workflow—covering tools, practices, and mindset shifts for 2025.

Cover image for: Build a Fun Alphabet Reader with TypeScript, Vite & Speech Synthesis API
Jun 27, 20254 MIN READ min read

Build a Fun Alphabet Reader with TypeScript, Vite & Speech Synthesis API

An interactive, educational project for beginners to learn modern frontend development.

|Made with · © 2026|TermsPrivacy
AboutBlogContact

Free, open-source tools for developers and creators · Community driven