npm package: @svazqz/next-api-generator
Description: A simple way to keep in sync Next api definitions and client types using zod
Author: Sergio Vazquez
Latest version: 2.2.0
Deprecated: Package renamed to @svazqz/api-contract-kit. Please migrate.

# 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:

```typescript
/* 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:

```typescript
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
```typescript
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
```typescript
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
```typescript
'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)
```typescript
'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](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=BTJPCXNPH43YC)