Building internal tools, you either love it or hate it. I've built a lot of tools over the years, usually from scratch, some with my own auto-generated UI libraries, but these days I rather don't waste too much time on it anymore. On the other hand I'm still not hooked on no-code platforms. I feel like writing some quick code on your existing services and business logic is usually faster and you don't have to share any secrets with 3rd parties. Setting up the boilerplate and user interfaces of internal tools tends to be most of the work, so as a developer I rather use a low-code alternative. When I saw interval, I was immediately hooked. It allows you to simply write async functions with your own backend code and then automatically generate a frontend that can be used by non-techies. I feel like combining this with WunderGraph is a match made in heaven. You can use WunderGraph to query all your backend services, like database and 3rd party APIs and then use Interval to build a UI for it. In this guide, we'll explore how to build an internal tool using WunderGraph and Interval. We'll build a simple tool that allows you to manage users. Getting started Head over to and create a new Interval app. We'll use the template for this guide, so we can leverage WunderGraphs typesafety. Interval typescript The create command includes your personal development key, so be sure to copy it from the Interval dashboard. npx create-interval-app --template=basic --language=ts --personal_development_key=eelco_dev_... Interval creates a new by default, if you choose another directory use that instead in the following steps. interval cd interval Next, we'll install WunderGraph. For this example we don't have any existing backend, so we'll start from scratch. npx create-wundergraph-app --init Great, now you can run the Interval app. yarn dev You'll see a message that says . This is the URL where you can access your Interval app. [Interval] 🔗 Connected! Access your actions at: https://interval.com/dashboard/eelco/develop Adding a WunderGraph API To make sure TypeScript operations work correctly we also need to make a small change to the tsconfig.json file. Copy tsconfig.json to tsconfig.build.json and make the following changes to tsconfig.json. { "compilerOptions": { "target": "esnext", "skipLibCheck": true, "strict": true, "esModuleInterop": true, "module": "commonjs" } } Let's make sure we run both Interval and Wundergraph with the dev command. First install , this allows us to run multiple commands in parallel. npm-run-all yarn add -D npm-run-all Now you can run and both Interval and WunderGraph will be running. yarn dev The WunderGraph example app comes with a couple of example operations, let's add them to the Interval app. We'll use the WunderGraph TypeScript client to do this. Edit and add the following lines to , this will generate the client into the interval src directory. .wundergraph/wundergraph.config.ts codeGenerators { templates: [templates.typescript.client], path: '../src/generated', }, Now let's add some routes to our App. Edit , you'll see there is just a route. Let's replace this with a route, that gets a user by id. src/index.ts hello_world User import Interval, { io } from '@interval/sdk' import 'dotenv/config' // loads environment variables from .env import { createClient } from './generated/client' const client = createClient() const interval = new Interval({ apiKey: process.env.INTERVAL_KEY, routes: { User: async () => { const id = await io.input.text('Enter a user id') const { data } = await client.query({ operationName: 'users/get', input: { id, }, }) return data }, }, }) interval.listen() Save the file and head over to the Interval dashboard. You'll see that the route is now available. You can click on it to test it out. User Let's say we want to be able to edit the user, we can do so by adding a link to a new editUser route. const interval = new Interval({ apiKey: process.env.INTERVAL_KEY, routes: { User: async () => { const id = await io.input.text('Enter a user id') const { data } = await client.query({ operationName: 'users/get', input: { id, }, }) if (!data) { await io.display.heading('User not found') return } await io.display.heading(data?.name, { description: data.bio, menuItems: [ { label: 'Edit user', action: 'EditUser', params: { userId: id }, }, ], }) }, EditUser: async () => { const id = ctx.params.userId as string const { data: user } = await client.query({ operationName: 'users/get', input: { id, }, }) if (!user) { io.display.heading('User not found') return } const [name, bio] = await io.group([ io.input.text('Name', { defaultValue: user.name }), io.input.text('Bio', { defaultValue: user.bio }), ]) await client.mutate({ operationName: 'users/update', input: { id, name, bio, }, }) }, }, }) To test this click on the User action and enter a user id. You'll see the user details and a link to edit the user. Click on the link and you'll be able to edit the user. When you click EditUser from the home screen, the action is completed, which is not very useful. We can fix this by adding a search. Create a new operation in and add the following code. .wundergraph/operations/users/search.ts import { createOperation, z } from '../../generated/wundergraph.factory' export default createOperation.query({ input: z.object({ query: z.string(), }), handler: async ({ input }) => { return [ { id: '1', name: 'Jens', bio: 'Founder of WunderGraph', }, { id: '2', name: 'Eelco', bio: 'Senior Developer at WunderGraph', }, ].filter((user) => input.query ? user.name.toLowerCase().includes(input.query.toLowerCase()) : true ) }, }) Now we can add a search to the EditUser route. EditUser: async () => { const id = ctx.params.userId as string const { data } = await client.query({ operationName: 'users/get', input: { id, }, }) let user = data if (!user) { user = await io.search('Search for a user', { renderResult: (user) => ({ label: user?.name || 'Not found', description: user?.bio, }), onSearch: async (query) => { const { data } = await client.query({ operationName: 'users/search', input: { query, }, }) return data || [] }, }) } if (!user) { return io.display.heading('User not found') } const [name, bio] = await io.group([ io.input.text('Name', { defaultValue: user.name }), io.input.text('Bio', { defaultValue: user.bio }), ]) await client.mutate({ operationName: 'users/update', input: { id, name, bio, }, }) } Awesome! We can now search and edit users. This was just a simple introduction to how you can use WunderGraph with Interval, but the possibilities are endless. By adding extra services to the WunderGraph virtual graph you can easily add more functionality to your Interval app. Like inviting users, refunding payments, creating flows to enrich data, etc. If you liked this post let me know in the comments or message me on , I'd love to go deeper into this topic in another post. Twitter Resources Interval docs WunderGraph docs Also Published Here