🌻 Support Ukraine Now! 🇺🇦

REST for Next.js

Transforms Next.js into a powerful and extensible REST API platform

1
Create
Create a static class and define API endpoints with decorators
// /src/modules/hello/HelloController.ts
import { get, prefix } from 'vovk';

@prefix('hello')
export default class HelloController {
/**
* Return a greeting from
* GET /api/hello/greeting
*/
@get('greeting')
static getHello() {
return { greeting: 'Hello world!' };
}
}
// /src/modules/hello/HelloController.ts
import { get, prefix } from 'vovk';

@prefix('hello')
export default class HelloController {
/**
* Return a greeting from
* GET /api/hello/greeting
*/
@get('greeting')
static getHello() {
return { greeting: 'Hello world!' };
}
}
2
Init
Initialise the controller at Next.js Optional Catch-all Segment
// /src/app/api/[[...vovk]]/route.ts
import { initVovk } from 'vovk';
import HelloController from '../../../modules/hello/HelloController';

export const runtime = 'edge'; // optional

// list of all controllers
const controllers = { HelloController };

// used to map types for the generated client library
export type Controllers = typeof controllers;

export const { GET, POST } = initVovk({ controllers });
// /src/app/api/[[...vovk]]/route.ts
import { initVovk } from 'vovk';
import HelloController from '../../../modules/hello/HelloController';

export const runtime = 'edge'; // optional

// list of all controllers
const controllers = { HelloController };

// used to map types for the generated client library
export type Controllers = typeof controllers;

export const { GET, POST } = initVovk({ controllers });
3
Use
Import the auto-generated fetching library from "vovk-client"
'use client';
import { useState } from 'react';
import { HelloController } from 'vovk-client';
import type { VovkClientReturnType } from 'vovk';

export default function Example() {
const [response, setResponse] = useState<VovkClientReturnType<typeof HelloController.getHello>>();

return (
<>
<button
onClick={async () => setResponse(
await HelloController.getHello()
)}
>
Get Greeting from the Server
</button>
<div>{response?.greeting}</div>
</>
);
}
'use client';
import { useState } from 'react';
import { HelloController } from 'vovk-client';
import type { VovkClientReturnType } from 'vovk';

export default function Example() {
const [response, setResponse] = useState<VovkClientReturnType<typeof HelloController.getHello>>();

return (
<>
<button
onClick={async () => setResponse(
await HelloController.getHello()
)}
>
Get Greeting from the Server
</button>
<div>{response?.greeting}</div>
</>
);
}
 
Hint: the endpoint for this example is implemented with generateStaticAPI and statically hosted on GitHub Pages. Rest of the examples below are served from the Examples Website API by using pre-built vovk-examples NPM package.

Quick Install

npx create-next-app -e https://github.com/finom/vovk-hello-world

Manual Install

Features

One Port

Run your full-stack app on a single port. Vovk.ts is an addon over documented Next.js API routes.

GET /

Good old REST API

No more workarounds and new protocols. Create RESTful API for your Next.js app with ease.

Easy to Learn

Specially designed for Next.js App Router, Vovk doesn't introduce complex abstractions by being just a wrapper over route handlers making it easy to learn.

Edge runtime

Edge runtime is available out of the box. Your REST API is geographically closer to users.

No dependencies

Vovk.ts is tiny and has zero dependencies.

Only TypeScript

Vovk.ts is a well-typed library that also exports types for any use-case imaginable.

Enjoy Mapped Types

You can jump straight from the client-side to the controller implementation, making the development process easier, thanks to Mapped Types in TypeScript.

Highly Customizable
import { UserController } from 'vovk-client';

// ...

UserController.createUser({
body,
query,
params,
successToast: 'Successfully created a new user',
useAuth: true,
sentryLogErrors: true,
});
import { UserController } from 'vovk-client';

// ...

UserController.createUser({
body,
query,
params,
successToast: 'Successfully created a new user',
useAuth: true,
sentryLogErrors: true,
});

You can completely redefine the behavior of the generated library by implementing your own fetching function. This allows tight integration with your application's state logic or the addition of extra options.

Read Docs
Back-end for React Native
import { GreetingController } from 'vovk-client';

// ...
<Pressable
onPress={async () => {
setGreetingResponse(
await GreetingController.getGreeting()
);
}}
>
<Text>Get Greeting</Text>
</Pressable>
import { GreetingController } from 'vovk-client';

// ...
<Pressable
onPress={async () => {
setGreetingResponse(
await GreetingController.getGreeting()
);
}}
>
<Text>Get Greeting</Text>
</Pressable>

Creating a full-stack React-Native application has never been so easy! Set up a project with React Native, Next.js, and Vovk.ts, and start making requests.

See example
Easy to Distribute
import { MyController } from 'my-client-library';

// ...

using stream = await MyController.streamTokens();

for await (const token of stream) {
console.log(token);
}
import { MyController } from 'my-client-library';

// ...

using stream = await MyController.streamTokens();

for await (const token of stream) {
console.log(token);
}

Bundle and distribute your REST API client library with ease. The examples below utilize the vovk-examples package, bundled with Webpack, which accesses REST endpoints from the Examples Website API.

See Examples Repository
Well-known API

Designed for the latest version of Next.js

The library designed for Next.js App Router It avoids introducing complex abstractions and does not modify the request object, acting merely as a wrapper over Next.js route handlers. It uses VovkRequest that extends NextRequest to define request body and search query in order to set up proper type recognition. If you're familiar with using them, you already know how to handle operations like retrieving the JSON body, Request Body FormData, search query, making a redirect, using cookies, accessing headers, etc.
Read Next.js Docs
import { type VovkRequest, prefix, put } from 'vovk';
import { redirect } from 'next/navigation';
import { headers } from 'next/headers';
// ...

@prefix('users')
export default class UserController {
@put(':id') // PUT /api/users/69?notifyOn=comment
@authGuard()
static async updateUser(
req: VovkRequest<Partial<User>, { notifyOn: 'comment' | 'none' }>,
{ id }: { id: string }
) {
const body = await req.json(); // type: Partial<User>
const notifyOn = req.nextUrl.searchParams.get('notifyOn'); // 'comment' | 'none'
// ...
redirect('/api/another/endpoint');
// ...
return updatedUser;
}
}
import { type VovkRequest, prefix, put } from 'vovk';
import { redirect } from 'next/navigation';
import { headers } from 'next/headers';
// ...

@prefix('users')
export default class UserController {
@put(':id') // PUT /api/users/69?notifyOn=comment
@authGuard()
static async updateUser(
req: VovkRequest<Partial<User>, { notifyOn: 'comment' | 'none' }>,
{ id }: { id: string }
) {
const body = await req.json(); // type: Partial<User>
const notifyOn = req.nextUrl.searchParams.get('notifyOn'); // 'comment' | 'none'
// ...
redirect('/api/another/endpoint');
// ...
return updatedUser;
}
}
import { UserController } from 'vovk-client';

// ...

const updatedUser = await UserController.updateUser({
params: { id: '69' },
body: { email: 'john@example.com' },
query: { notifyOn: 'comment' },
});
import { UserController } from 'vovk-client';

// ...

const updatedUser = await UserController.updateUser({
params: { id: '69' },
body: { email: 'john@example.com' },
query: { notifyOn: 'comment' },
});
Code Splitting

Embracing the Service-Controller Pattern

Drawing inspiration from NestJS, this library champions the well-known Service-Controller pattern, distinctly separating database and API requests from the code managing incoming requests. This design promotes cleaner, more organized code structures, thereby enhancing maintainability and scalability. Check out the full example here.
Read Docs
// /src/modules/hello/HelloService.ts
export default class HelloService {
static getHello() {
return { greeting: 'Hello world!' };
}
}
// /src/modules/hello/HelloService.ts
export default class HelloService {
static getHello() {
return { greeting: 'Hello world!' };
}
}
// /src/modules/hello/HelloController.ts
import { get, prefix } from "vovk";
import HelloService from "./HelloService";

@prefix('hello')
export default class HelloController {
private static helloService = HelloService;

@get('greeting')
static getHello() {
return this.helloService.getHello();
}
}
// /src/modules/hello/HelloController.ts
import { get, prefix } from "vovk";
import HelloService from "./HelloService";

@prefix('hello')
export default class HelloController {
private static helloService = HelloService;

@get('greeting')
static getHello() {
return this.helloService.getHello();
}
}
Response Streaming

Stream Server Responses with Async Generators and Disposable Objects

Vovk.ts meets the contemporary demand for streaming responses in AI client libraries, leveraging modern TypeScript syntax. Explore how it implements OpenAI chat completion with response streaming:
Check the full code of this example on the examples website. You might also find other streaming examples interesting:
Read Docs
// /src/modules/openai/OpenAiController.ts
import { type VovkRequest, post, prefix } from 'vovk';
import OpenAI from 'openai';

@prefix('openai')
export default class OpenAiController {
private static openai = new OpenAI();

@post('chat')
static async *createChatCompletion(
req: VovkRequest<{ messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] }>
) {
const { messages } = await req.json();

yield* await this.openai.chat.completions.create({
messages,
model: 'gpt-3.5-turbo',
stream: true,
});
}
}
// /src/modules/openai/OpenAiController.ts
import { type VovkRequest, post, prefix } from 'vovk';
import OpenAI from 'openai';

@prefix('openai')
export default class OpenAiController {
private static openai = new OpenAI();

@post('chat')
static async *createChatCompletion(
req: VovkRequest<{ messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] }>
) {
const { messages } = await req.json();

yield* await this.openai.chat.completions.create({
messages,
model: 'gpt-3.5-turbo',
stream: true,
});
}
}
import { OpenAiController } from 'vovk-client';

// ...

using completion = await OpenAiController.createChatCompletion({
body: { messages },
});

for await (const chunk of completion) {
console.log(chunk);
}
import { OpenAiController } from 'vovk-client';

// ...

using completion = await OpenAiController.createChatCompletion({
body: { messages },
});

for await (const chunk of completion) {
console.log(chunk);
}
Request Validation

Isomorphic Validation

Vovk.ts provides an easy way to validate requests using Zod, a TypeScript-first schema declaration and validation library. This is achieved through the use of vovkZod decorator implemented at vovk-zod package. The validation is isomorphic, meaning it works on both the server and the client and the data is validated before it reaches the server. Try it yourself:
See the code for this example here. The examples website also includes React Hook Form example that you can view by this link.
Read Docs
// /src/modules/user/FormController.ts
import { prefix, post, VovkRequest } from 'vovk';
import vovkZod from 'vovk-zod';
import { z } from 'zod';
import { userSchema } from '../../zod';

@prefix('form')
export default class FormController {
@post('create-user')
@vovkZod(userSchema)
static async createUser(req: VovkRequest<z.infer<typeof userSchema>>) {
const { firstName, lastName, email } = await req.json();

return {
success: true,
user: { firstName, lastName, email },
};
}
}
// /src/modules/user/FormController.ts
import { prefix, post, VovkRequest } from 'vovk';
import vovkZod from 'vovk-zod';
import { z } from 'zod';
import { userSchema } from '../../zod';

@prefix('form')
export default class FormController {
@post('create-user')
@vovkZod(userSchema)
static async createUser(req: VovkRequest<z.infer<typeof userSchema>>) {
const { firstName, lastName, email } = await req.json();

return {
success: true,
user: { firstName, lastName, email },
};
}
}
'use client';
import { useState, type FormEvent } from 'react';
import { FormController } from 'vovk-client';

export default function FormExample() {
// ...
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
await FormController.createUser({
body: { firstName, lastName, email },
})
};

return (
<form onSubmit={onSubmit}>
{/* ... */}
</form>
);
}

'use client';
import { useState, type FormEvent } from 'react';
import { FormController } from 'vovk-client';

export default function FormExample() {
// ...
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
await FormController.createUser({
body: { firstName, lastName, email },
})
};

return (
<form onSubmit={onSubmit}>
{/* ... */}
</form>
);
}

Client-side threading

Bonus Feature: Seamless Web Workers Invocation

Vovk.ts provides an easy way to integrate Web Workers into your application. This feature allows you to offload heavy calculations to a separate browser thread, preventing the main thread from becoming unresponsive. The library simplifies the process of creating and using Web Workers, making it accessible without setting up messaging manually to exchange data between threads.
View the full code of this example here. You might be also interested to see how you would implement continious event streaming with generators that is illustrated as approximation of π with BigInt.

This feature is also implemented with TypeScript Mapped Types so you can jump straight from the main thread to the worker implementation.
Read Docs
// /src/modules/hello/HelloWorker.ts
import { worker } from 'vovk';

@worker()
export default class HelloWorker {
static factorize(number: bigint): bigint[] {
let factors: bigint[] = [];
// ...

return factors;
}
}
// /src/modules/hello/HelloWorker.ts
import { worker } from 'vovk';

@worker()
export default class HelloWorker {
static factorize(number: bigint): bigint[] {
let factors: bigint[] = [];
// ...

return factors;
}
}
'use client';
import { useEffect, useState, type FormEvent } from 'react';
import { HelloWorker } from 'vovk-client';

export default function WorkerExample() {
const [value, setValue] = useState('123456789');
const [result, setResult] = useState<bigint[]>();

useEffect(() => {
// inject the worker to the generated interface
HelloWorker.use(new Worker(new URL('../../modules/worker/HelloWorker.ts', import.meta.url)));
}, []);

const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setResult(await HelloWorker.factorize(BigInt(value)));
};

return (
<form onSubmit={onSubmit}>
{/* ... */}
</form>
);
}
'use client';
import { useEffect, useState, type FormEvent } from 'react';
import { HelloWorker } from 'vovk-client';

export default function WorkerExample() {
const [value, setValue] = useState('123456789');
const [result, setResult] = useState<bigint[]>();

useEffect(() => {
// inject the worker to the generated interface
HelloWorker.use(new Worker(new URL('../../modules/worker/HelloWorker.ts', import.meta.url)));
}, []);

const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setResult(await HelloWorker.factorize(BigInt(value)));
};

return (
<form onSubmit={onSubmit}>
{/* ... */}
</form>
);
}

Sponsors

Sponsor the author of this project on Github ♥️
You can also contact me via email from my Github profile