https://agrume.js.org
API development made for front-end developers!
Easy, customizable and type-safe.
Front-end developers are often afraid of the backend. They don't know how to start, what to do, and how to do it. Agrume is a tool that makes developing API endpoints as easy as writing a function. Best of all, it's type-safe!
Let's see an example:
import { createRoute } from 'agrume'
const getDogImage = createRoute(
async () => {
// `database` is a fake database that should not be accessible from the client
const dog = database.dogs.findFirst({
select: ['imageBase64'],
where: { isGoodBoy: true }
})
return dog.imageBase64
}
)
export const Dog = function () {
const [dogImage, setDogImage] = useState('')
useEffect(() => {
getDogImage().then(setDogImage)
}, [])
return <img src={dogImage} />
}
As a student, I frequently have to build projects in teams and in a short amount of time. These projects require a backend, but many of my teammates prefer to work on the frontend because they are not comfortable with the backend. I wanted to create a tool that would make backend development as easy as frontend development, so that it would be easier to organise the work in teams.
I think that Agrume is great to build prototypes and small projects. However, I don't know if it's a good idea to use it in production. I would love to hear your feedback on this!
pnpm add agrume vite-plugin-agrume
Note
Agrume is agnostic. This means that you can use it with the stack of your choice. However, for now we only provide a Vite plugin.
Now, you can add the plugin to your vite.config.ts
:
import { defineConfig } from 'vite'
import { agrume } from 'vite-plugin-agrume'
export default defineConfig({
plugins: [
agrume()
// ...
]
})
Warning
In some cases, you need to add the plugin to the top of the list of plugins. For example, if you use Vite React, the Vite React plugin will add side-effect statements to your code, which will break Agrume. To work around this problem, you can also use the createRoute
function in separate files.
Note
If you want to make Agrume work with another stack, you may want to use the babel plugin. Feel free to open a PR to add support for your stack!
Agrume is designed to be as simple as possible. It doesn't need any configuration to work. However, you can configure it to suit your needs.
By default, Agrume will prefix all your routes with /api
. You can change this prefix by passing the prefix
option to the plugin:
// ...
export default defineConfig({
plugins: [
agrume({
prefix: '/my-api'
})
// ...
]
})
By default, Agrume will use the Vite dev server to serve your API. However, you can use your own server by passing the useMiddleware
option to the plugin:
// ...
import { server } from './server'
export default defineConfig({
plugins: [
agrume({
useMiddleware: server.use.bind(server),
})
// ...
]
})
The useMiddleware
option takes a function that takes a Connect-like middleware as an argument. Here is an example of a Connect-like server:
import { createServer } from "node:http"
import connect from "connect"
const app = connect()
const server = createServer(app)
server.listen(3000)
export { app as server }
Many backend frameworks can use Connect-like middleware. For example, Express can use Connect-like middleware. You can use it as a server:
import express from 'express'
const app = express()
const server = app.listen(3000)
export { app as server }
But please, don't use Express. See "Why you should drop ExpressJS" by Romain Lanz.
By default, Agrume does not log anything. However, you can pass a logger to the plugin to log the requests:
// ...
export default defineConfig({
plugins: [
agrume({
logger: {
info: console.info,
error: console.error,
}
})
// ...
]
})
You can use fs.writeFileSync
instead of console.log
to log the requests to a file.
// ...
export default defineConfig({
plugins: [
agrume({
logger: {
info: (...args) => fs.writeFileSync('info.log', args.join(' ') + '\n', { flag: 'a' }),
error: (...args) => fs.writeFileSync('error.log', args.join(' ') + '\n', { flag: 'a' }),
}
})
// ...
]
})
The only thing you need to create a route is the createRoute
function. It takes a function as an argument and returns a function that can be called to do a request to the route.
import { createRoute } from 'agrume'
const sayHello = createRoute(
async () => {
return 'Hello world!'
},
)
Note
sayHello
will be typed as () => Promise<string>
.
You can then use the sayHello
function to do a request to the route:
sayHello().then(console.log) // Hello world!
Warning
At the moment you can only use the createRoute
function in .js
, .jsx
, .ts
and .tsx
files. To use Agrume in other files, you need to export the createRoute
function from one of the valid files and import it into the other files. (See Vue example)
You can request parameters from the client just like you would do with a normal function:
import { createRoute } from 'agrume'
const sayHello = createRoute(
async (name: string) => {
return `Hello ${name}!`
},
)
You can then use the sayHello
function to do a request to the route:
sayHello('Arthur').then(console.log) // Hello Arthur!
Note
Agrume is type-safe so if you don't pass the correct parameters to the function, your IDE will warn you!
Note
Agrume will pass the parameters to the server as body parameters so every request will be a POST
request.
You can configure each route individually by passing an object to the createRoute
function.
You can specify the path of the route by passing a string starting with /
to the path
option:
import { createRoute } from 'agrume'
const getDogImage = createRoute(
async () => {}, {
path: '/dog'
},
)
By default, Agrume will transform the createRoute
function into a function that can be called to do a request to the route. The default client will use the fetch
API to do the request. However, you can specify your own client by passing a function to the getClient
option.
For example, if you want use a custom server that listen on port 3000
, you can do:
import { createRoute } from 'agrume'
const getDogImage = createRoute(
async () => {},
{
getClient(requestOptions) {
return async (parameters) => {
const response = await fetch(
`http://localhost:3000${requestOptions.url}`,
{
...requestOptions,
body: JSON.stringify(parameters)
}
)
return response.json()
}
}
},
)
Note
The parameters
argument cannot be inferred by TypeScript, so it will be typed as any
. You can type it yourself, but it must be the same type as the parameters of the route.
Important
getClient
will affect the type of the route. For example, if your getClient
function returns the requestOptions
, the type of the route will be () => Promise<RequestOptions>
.
Have a look at the Recipes section to see what you can do with the getClient
option.
You can use the getClient
option to add authentication to your routes. For example, if you want to add a JWT authentication to your routes, you can do:
import { createRoute } from 'agrume'
const getUser = ({ token }) => {
// ...
}
const getAgrumeClient = (requestOptions) => {
return async (parameters) => {
const token = localStorage.getItem('token')
const response = await fetch(
`http://localhost:3000${requestOptions.url}`,
{
...requestOptions,
body: JSON.stringify({
...parameters,
token
})
}
)
return response.json()
}
}
const authenticatedRoute = createRoute(
async (parameters) => {
const user = await getUser(parameters)
return user
},
{
getClient: getAgrumeClient
},
)
You can find examples in the examples directory.