npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@svazqz/next-api-generator

v2.2.0

Published

A simple way to keep in sync Next api definitions and client types using zod

Readme

next-api-generator

next-api-generator is an project designed to streamline the development of new Next.js applications and their accompanying documentation. Based on Typescript, Next.js, Zod, ReactQuery, and the zod-to-openapi library, next-api-generator significantly reduces the overhead typically associated with these processes.

Getting started

Prerequisites

  • pnpm v8.6.11
  • node v18.17.0
  • vscode

Core Concepts

Schema

At the heart of next-api-generator is the Schema, a ZodObject that defines the structure for key elements of an API request, including query parameters, URL parameters, rquest body, and response. This robust schema validation ensures consistency and reliability across your application. The schemas will be located on each app acording to the domain of each app. This way every app can set their own scope foe each schema. An example of this schema could be app/data/geo/schemas.ts containing:

/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-namespace */
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';

extendZodWithOpenApi(z);
export namespace GeoDefinitions {
  export namespace Schemas {
    export const Coordinates = z
      .object({
        latitude: z.number(),
        longitude: z.number(),
      })
      .openapi('Coordinates');

    export const LocationData = z
      .object({
        city: z.string(),
        state: z.string(),
        country: z.string(),
      })
      .openapi('LocationData');
  }

  export namespace Types {
    export type Coordinates = z.infer<typeof Schemas.Coordinates>;
    export type LocationData = z.infer<typeof Schemas.LocationData>;
  }
}

Request Handler

next-api-generator proposes a request/api driven development, this means that all the api endpoins are defined first setting the input, output, params and query formats so when handler function is defined it has access to auto complete features and the same happens with consumer. An example of a request definition can be as follow:

import { createAPIDefinition } from '@svazqz/next-api-generator';
import { GeoDefinitions } from './schemas';

export const getGeoData = createAPIDefinition({
  endpoint: '/geo',
  schemas: {
    queryParams: GeoDefinitions.Schemas.Coordinates,
    response: GeoDefinitions.Schemas.LocationData,
  },
});

export const postGeoData = createAPIDefinition({
  method: 'post',
  endpoint: '/geo',
  schemas: {
    payload: GeoDefinitions.Schemas.Coordinates,
    response: GeoDefinitions.Schemas.LocationData,
  },
});

And then used in the Next.js API route definition as follows:

Next.js API Routes with Typed Parameters

import { nextAPIWrapper } from '@svazqz/next-api-generator/server';
import { getGeoData, postGeoData } from '../../../data/geo/api';

export const GET = nextAPIWrapper(getGeoData, async (request, queryParams, urlParams) => {
  const { latitude, longitude } = queryParams;
  
  const locationResponse = await fetch(
    `https://geocode.xyz/${latitude},${longitude}?json=1`,
  );
  const locationData = await locationResponse.json();
  const fullData = locationData.standard || locationData;
  
  return {
    city: fullData.city || 'Unknown',
    state: fullData.state || 'Unknown',
    country: fullData.country || 'Unknown',
  };
});

export const POST = nextAPIWrapper(
  postGeoData,
  async (request, queryParams, urlParams, body) => {
    const { latitude, longitude } = body!;
    
    const locationResponse = await fetch(
      `https://geocode.xyz/${latitude},${longitude}?json=1`,
    );
    const locationData = await locationResponse.json();
    const fullData = locationData.standard || locationData;
    
    return {
      city: fullData.city || 'Unknown',
      state: fullData.state || 'Unknown',
      country: fullData.country || 'Unknown',
    };
  },
);

Express.js API Routes with Typed Parameters

const express = require('express');
const { expressAPIWrapper } = require('@svazqz/next-api-generator/server');
const { getGeoData, postGeoData } = require('../data/geo/api');

const app = express();
app.use(express.json());

// GET route with typed query parameters
app.get('/api/geo', expressAPIWrapper(
  getGeoData,
  async (req, res, queryParams, urlParams) => {
    const { latitude, longitude } = queryParams;
    
    const locationResponse = await fetch(
      `https://geocode.xyz/${latitude},${longitude}?json=1`,
    );
    const locationData = await locationResponse.json();
    const fullData = locationData.standard || locationData;
    
    return {
      city: fullData.city || 'Unknown',
      state: fullData.state || 'Unknown',
      country: fullData.country || 'Unknown',
    };
  }
));

// POST route with typed body parameters
app.post('/api/geo', expressAPIWrapper(
  postGeoData,
  async (req, res, queryParams, urlParams, body) => {
    const { latitude, longitude } = body;
    
    const locationResponse = await fetch(
      `https://geocode.xyz/${latitude},${longitude}?json=1`,
    );
    const locationData = await locationResponse.json();
    const fullData = locationData.standard || locationData;
    
    return {
      city: fullData.city || 'Unknown',
      state: fullData.state || 'Unknown',
      country: fullData.country || 'Unknown',
    };
  }
));

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Handler Signature

Both nextAPIWrapper and expressAPIWrapper provide your handler function with typed parameters:

  • request: The original framework request object (NextRequest for Next.js, Express req for Express.js)
  • queryParams: Validated and typed query parameters based on your schema
  • urlParams: Validated and typed URL parameters based on your schema
  • body: Validated and typed request body (for POST/PUT requests) based on your schema

The wrappers automatically:

  • Validate URL parameters, query parameters and request body against your Zod schemas
  • Parse and type the parameters before passing them to your handler
  • Handle validation errors with appropriate HTTP status codes
  • Validate the response against your response schema

Framework Support

next-api-generator now supports both Next.js and Express.js:

  • Next.js: Use nextAPIWrapper for Next.js 13+ App Router compatibility
  • Express.js: Use expressAPIWrapper for Express.js applications
  • Backward Compatibility: apiWrapper is an alias for nextAPIWrapper

Consumer

next-api-generator provides multiple ways to consume APIs on the client side. You can use either the React Query integration for automatic caching and state management, or the simpler promise-based consumer for direct API calls.

Option 1: Simple Promise-based Consumer

'use client';
import { ChangeEvent, useState } from 'react';
import { apiConsumer } from '@svazqz/next-api-generator/client';
import { postGeoData, getGeoData } from '../data/geo/api';

const GeoData = function GeoData() {
  const [latitude, setLatitude] = useState(19.3906594);
  const [longitude, setLongitude] = useState(-99.308425);
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState<any>(null);
  const [error, setError] = useState<string | null>(null);

  const fetchData = async (usePost = true) => {
    setLoading(true);
    setError(null);
    try {
      if (usePost) {
        const postConsumer = apiConsumer(postGeoData);
        const result = await postConsumer({
          body: { latitude, longitude }
        });
        setData(result);
      } else {
        const getConsumer = apiConsumer(getGeoData);
        const result = await getConsumer({
          query: { latitude, longitude }
        });
        setData(result);
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
    } finally {
      setLoading(false);
    }
  };

  const onChangeLatitude = (ev: ChangeEvent<HTMLInputElement>) => {
    setLatitude(Number(ev.target.value));
  };

  const onChangeLongitude = (ev: ChangeEvent<HTMLInputElement>) => {
    setLongitude(Number(ev.target.value));
  };

  return (
    <div className="p-4">
      <input
        type="number"
        placeholder="latitude"
        onChange={onChangeLatitude}
        value={latitude}
        className="border p-2 mr-2"
      />
      <input
        type="number"
        placeholder="longitude"
        onChange={onChangeLongitude}
        value={longitude}
        className="border p-2 mr-2"
      />
      <button 
        onClick={() => fetchData(true)}
        disabled={loading}
        className="bg-blue-500 text-white p-2 mr-2"
      >
        POST Request
      </button>
      <button 
        onClick={() => fetchData(false)}
        disabled={loading}
        className="bg-green-500 text-white p-2"
      >
        GET Request
      </button>
      
      {loading && <div>Loading...</div>}
      {error && <div className="text-red-500">Error: {error}</div>}
      {data && (
        <textarea 
          readOnly
          value={JSON.stringify(data, null, 2)}
          className="w-full h-32 mt-4 p-2 border"
        />
      )}
    </div>
  );
};

export default GeoData;

Option 2: React Query Integration (Advanced)

'use client';
import { ChangeEvent, useState } from 'react';
import { apiConsumerClient } from '@svazqz/next-api-generator/client';
import { postGeoData } from '../data/geo/api';

const geoDataConsumer = apiConsumerClient(postGeoData);

const GeoData = function GeoData() {
  const [latitude, setLatitude] = useState(19.3906594);
  const [longitude, setLongitude] = useState(-99.308425);
  
  const { query, queryKey } = geoDataConsumer(
    {
      body: {
        latitude,
        longitude,
      },
    },
    {
      enabled: false, // Disable automatic fetching
    },
  );

  const onChangeLatitude = (ev: ChangeEvent<HTMLInputElement>) => {
    setLatitude(Number(ev.target.value));
  };

  const onChangeLongitude = (ev: ChangeEvent<HTMLInputElement>) => {
    setLongitude(Number(ev.target.value));
  };

  if (query.isLoading) return <div>Loading...</div>;
  if (query.error) return <div>Error: {query.error.message}</div>;

  return (
    <div className="p-4">
      <input
        type="number"
        placeholder="latitude"
        onChange={onChangeLatitude}
        value={latitude}
        className="border p-2 mr-2"
      />
      <input
        type="number"
        placeholder="longitude"
        onChange={onChangeLongitude}
        value={longitude}
        className="border p-2 mr-2"
      />
      <button 
        onClick={() => query.refetch()}
        className="bg-blue-500 text-white p-2"
      >
        Fetch Data
      </button>
      
      {query.data && (
        <textarea 
          readOnly
          value={JSON.stringify(query.data, null, 2)}
          className="w-full h-32 mt-4 p-2 border"
        />
      )}
      
      <div className="mt-2 text-sm text-gray-600">
        Query Key: {JSON.stringify(queryKey)}
      </div>
    </div>
  );
};

export default GeoData;

Note: For the React Query integration (Option 2), make sure to wrap your app with a QueryClientProvider as shown in the setup section.

Donate

paypal