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 🙏

© 2024 – Pkg Stats / Ryan Hefner

spiceflow

v1.1.18

Published

Simple API framework with RPC and type safety

Downloads

270

Readme

Spiceflow

Spiceflow is a lightweight, type-safe API framework for building web services using modern web standards.

Features

  • Type safety
  • OpenAPI compatibility
  • RPC client generation
  • Simple and intuitive API
  • Uses web standards for requests and responses
  • Supports async generators for streaming
  • Modular design with .use() for mounting sub-apps
  • Base path support

Installation

npm install spiceflow

Basic Usage

import { Spiceflow } from 'spiceflow'

const app = new Spiceflow()
  .get('/hello', () => 'Hello, World!')
  .post('/echo', async ({ request }) => {
    const body = await request.json()
    return body
  })

app.listen(3000)

Notice that you should never declare app separately and add routes later, that way you lose the type safety. Instead always declare all routes in one place.

// This is an example of what NOT to do when using Spiceflow

import { Spiceflow } from 'spiceflow'

// Do NOT declare the app separately and add routes later
const app = new Spiceflow()

// Do NOT do this! Adding routes separately like this will lose type safety
app.get('/hello', () => 'Hello, World!')
app.post('/echo', async ({ request }) => {
  const body = await request.json()
  return body
})

Requests and Responses

POST Request with Body Schema

import { z } from 'zod'
import { Spiceflow } from 'spiceflow'

new Spiceflow().post(
  '/users',
  async ({ request }) => {
    const body = await request.json() // here body has type { name: string, email: string }
    return `Created user: ${body.name}`
  },
  {
    body: z.object({
      name: z.string(),
      email: z.string().email(),
    }),
  },
)

Notice that to get the body of the request, you need to call request.json() to parse the body as JSON. Spiceflow does not parse the Body automatically, there is no body field in the Spiceflow route argument, instead you call either request.json() or request.formData() to get the body and validate it at the same time. This works by wrapping the request in a SpiceflowRequest instance, which has a json() and formData() method that parse the body and validate it. The returned data will have the correct schema type instead of any.

Response Schema

import { z } from 'zod'
import { Spiceflow } from 'spiceflow'

new Spiceflow().get(
  '/users/:id',
  ({ request, params }) => {
    const typedJson = await request.json() // this body will have the correct type
    return { id: Number(params.id), name: typedJson.name }
  },
  {
    body: z.object({
      name: z.string(),
    }),
    response: z.object({
      id: z.number(),
      name: z.string(),
    }),
    params: z.object({
      id: z.string(),
    }),
  },
)

Generate RPC Client

import { createSpiceflowClient } from 'spiceflow/client'
import { Spiceflow } from 'spiceflow'
import { z } from 'zod'

// Define the app with multiple routes and features
const app = new Spiceflow()
  .get('/hello/:id', ({ params }) => `Hello, ${params.id}!`)
  .post(
    '/users',
    async ({ request }) => {
      const body = await request.json() // here body has type { name?: string, email?: string }
      return `Created user: ${body.name}`
    },
    {
      body: z.object({
        name: z.string().optional(),
        email: z.string().email().optional(),
      }),
    },
  )
  .get('/stream', async function* () {
    yield 'Start'
    await new Promise((resolve) => setTimeout(resolve, 1000))
    yield 'Middle'
    await new Promise((resolve) => setTimeout(resolve, 1000))
    yield 'End'
  })

// Create the client
const client = createSpiceflowClient<typeof app>('http://localhost:3000')

// Example usage of the client
async function exampleUsage() {
  // GET request
  const { data: helloData, error: helloError } = await client
    .hello({ id: 'World' })
    .get()
  if (helloError) {
    console.error('Error fetching hello:', helloError)
  } else {
    console.log('Hello response:', helloData)
  }

  // POST request
  const { data: userData, error: userError } = await client.users.post({
    name: 'John Doe',
    email: '[email protected]',
  })
  if (userError) {
    console.error('Error creating user:', userError)
  } else {
    console.log('User creation response:', userData)
  }

  // Async generator (streaming) request
  const { data: streamData, error: streamError } = await client.stream.get()
  if (streamError) {
    console.error('Error fetching stream:', streamError)
  } else {
    for await (const chunk of streamData) {
      console.log('Stream chunk:', chunk)
    }
  }
}

Mounting Sub-Apps

import { Spiceflow } from 'spiceflow'
import { z } from 'zod'

const mainApp = new Spiceflow()
  .post(
    '/users',
    async ({ request }) => `Created user: ${(await request.json()).name}`,
    {
      body: z.object({
        name: z.string(),
      }),
    },
  )
  .use(new Spiceflow().get('/', () => 'Users list'))

Base Path

import { Spiceflow } from 'spiceflow'

const app = new Spiceflow({ basePath: '/api/v1' })
app.get('/hello', () => 'Hello') // Accessible at /api/v1/hello

Async Generators (Streaming)

Async generators will create a server sent event response.

import { Spiceflow } from 'spiceflow'

const app = new Spiceflow().get('/sseStream', async function* () {
  yield { message: 'Start' }
  await new Promise((resolve) => setTimeout(resolve, 1000))
  yield { message: 'Middle' }
  await new Promise((resolve) => setTimeout(resolve, 1000))
  yield { message: 'End' }
})

// Server-Sent Events (SSE) format
// The server will send events in the following format:
// data: {"message":"Start"}
// data: {"message":"Middle"}
// data: {"message":"End"}

// Example response output:
// data: {"message":"Start"}
// data: {"message":"Middle"}
// data: {"message":"End"}

// Client usage example with RPC client
import { createSpiceflowClient } from 'spiceflow/client'

const client = createSpiceflowClient<typeof app>('http://localhost:3000')

async function fetchStream() {
  const response = await client.sseStream.get()
  if (response.error) {
    console.error('Error fetching stream:', response.error)
  } else {
    for await (const chunk of response.data) {
      console.log('Stream chunk:', chunk)
    }
  }
}

fetchStream()

Error Handling

import { Spiceflow } from 'spiceflow'

new Spiceflow().onError(({ error }) => {
  console.error(error)
  return new Response('An error occurred', { status: 500 })
})

Middleware

import { Spiceflow } from 'spiceflow'

new Spiceflow().use(({ request }) => {
  console.log(`Received ${request.method} request to ${request.url}`)
})

Modifying Response with Middleware

Middleware in Spiceflow can be used to modify the response before it's sent to the client. This is useful for adding headers, transforming the response body, or performing any other operations on the response.

Here's an example of how to modify the response using middleware:

import { Spiceflow } from 'spiceflow'

new Spiceflow()
  .use(async ({ request }, next) => {
    const response = await next()
    if (response) {
      // Add a custom header to all responses
      response.headers.set('X-Powered-By', 'Spiceflow')
    }
    return response
  })
  .get('/example', () => {
    return { message: 'Hello, World!' }
  })

Generating OpenAPI Schema

import { openapi } from 'spiceflow/openapi'
import { Spiceflow } from 'spiceflow'
import { z } from 'zod'

const app = new Spiceflow()
  .use(openapi({ path: '/openapi.json' }))
  .get('/hello', () => 'Hello, World!', {
    query: z.object({
      name: z.string(),
      age: z.number(),
    }),
    response: z.string(),
  })
  .post(
    '/user',
    () => {
      return new Response('Hello, World!')
    },
    {
      body: z.object({
        name: z.string(),
        email: z.string().email(),
      }),
    },
  )

const openapiSchema = await (
  await app.handle(new Request('http://localhost:3000/openapi.json'))
).json()

Adding CORS Headers

import { cors } from 'spiceflow/cors'
import { Spiceflow } from 'spiceflow'

const app = new Spiceflow().use(cors()).get('/hello', () => 'Hello, World!')

Proxy requests

import { Spiceflow } from 'spiceflow'
import { MiddlewareHandler } from 'spiceflow/dist/types'

const app = new Spiceflow()

function createProxyMiddleware({
  target,
  changeOrigin = false,
}): MiddlewareHandler {
  return async (context) => {
    const { request } = context
    const url = new URL(request.url)

    const proxyReq = new Request(
      new URL(url.pathname + url.search, target),
      request,
    )

    if (changeOrigin) {
      proxyReq.headers.set('origin', new URL(target).origin || '')
    }
    console.log('proxying', proxyReq.url)
    const res = await fetch(proxyReq)

    return res
  }
}

app.use(
  createProxyMiddleware({
    target: 'https://api.openai.com',
    changeOrigin: true,
  }),
)

// or with a basePath
app.use(
  new Spiceflow({ basePath: '/v1/completions' }).use(
    createProxyMiddleware({
      target: 'https://api.openai.com',
      changeOrigin: true,
    }),
  ),
)

app.listen(3030)

Authorization Middleware

You can handle authorization in a middleware, for example here the code checks if the user is logged in and if not, it throws an error. You can use the state to track request data, in this case the state keeps a reference to the session.

import { z } from 'zod'
import { Spiceflow } from 'spiceflow'

new Spiceflow()
  .state('session', null as Session | null)
  .use(async ({ request: req, state }, next) => {
    const res = new Response()

    const { session } = await getSession({ req, res })
    if (!session) {
      return
    }
    state.session = session
    const response = await next()

    const cookies = res.headers.getSetCookie()
    for (const cookie of cookies) {
      response.headers.append('Set-Cookie', cookie)
    }

    return response
  })
  .post('/protected', async ({ state }) => {
    const { session } = state
    if (!session) {
      throw new Error('Not logged in')
    }
    return { ok: true }
  })