@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
nextAPIWrapperfor Next.js 13+ App Router compatibility - Express.js: Use
expressAPIWrapperfor Express.js applications - Backward Compatibility:
apiWrapperis an alias fornextAPIWrapper
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.

