REST API example with Express.js
This tutorial will guide you through the process of creating a REST API with Express.js that uses the Golem Network to process data. The goal is to show you how to use the Job API in a real-world scenario.
What will you build?
You will create a simple REST API that will allow you to send some text to the Golem Network and get back a text-to-speech result in the form of a WAV file.
Prerequisites
This tutorial assumes that you have already installed Yagna and have it running in the background. If you haven't done so yet, please follow the instructions in this tutorial before proceeding.
Setting up the project
First, create a new directory for your project and initialize a new Node.js project in it:
mkdir golem-express
cd golem-express
npm init -y
npm install @golem-sdk/golem-js express
Creating the API
Let's start by creating a new file called index.mjs
and pasting the following code into it:
import express from 'express'
const app = express()
const port = 3000
app.use(express.text())
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
Start your app by running node index.mjs
and run curl http://localhost:3000
in another terminal window. You should see the following output:
Hello World!
So far, so good! Stop the app by pressing Ctrl+C
and let's move on to the next step.
Connecting to the Golem Network
Now let's connect to the Golem Network. First, import the GolemNetwork
class from @golem-sdk/golem-js
and create a new instance of it.
import { GolemNetwork } from '@golem-sdk/golem-js'
const golemClient = new GolemNetwork({
yagna: {
apiKey: 'try_golem',
},
})
await golemClient
.init()
.then(() => {
console.log('Connected to the Golem Network!')
})
.catch((error) => {
console.error('Failed to connect to the Golem Network:', error)
process.exit(1)
})
Let's also add a handler for the SIGINT
signal so that we can close the connection to the Golem Network and cancel all running jobs when the user presses Ctrl+C
:
process.on('SIGINT', async () => {
// cancel and cleanup all running jobs
await golemClient.close()
process.exit(0)
})
Creating a retrievable task
Now it's time for the fun part! Let's add a new endpoint to our API that will take some text from the request body and create a new job on the Golem Network. To do that, we will use the createJob()
method. This method will give us a Job
object that we can use to get the state of the job and its results later. On the provider side, we will run the espeak
command to convert the text to speech, save it to a .wav
file and download that file to your local file system with the downloadFile()
method. We will give the file a random name to avoid collisions.
The image we will use is severyn/espeak:latest
, provided by one of our community members. It contains the espeak
command, which we will use to convert the text to speech. If you're feeling adventurous, you can create your own image and install espeak on it.
app.post('/tts', async (req, res) => {
if (!req.body) {
res.status(400).send('Missing text parameter')
return
}
const job = golemClient.createJob({
package: {
imageTag: 'severyn/espeak:latest',
},
})
job.startWork(async (ctx) => {
const fileName = `${Math.random().toString(36).slice(2)}.wav`
await ctx
.beginBatch()
.run(`espeak "${req.body}" -w /golem/output/output.wav`)
.downloadFile('/golem/output/output.wav', `public/${fileName}`)
.end()
return fileName
})
res.send(`Job started! ID: ${job.id}`)
})
The Job
api makes it easy to react to events that happen during the execution of the job. Let's update our code and add some event handlers to log the events to the console:
app.post('/tts', async (req, res) => {
if (!req.body) {
res.status(400).send('Missing text parameter')
return
}
const job = golemClient.createJob({
package: {
imageTag: 'severyn/espeak:latest',
},
})
job.events.on('created', () => {
console.log('Job created')
})
job.events.on('started', () => {
console.log('Job started')
})
job.events.on('error', () => {
console.log('Job failed', job.error)
})
job.events.on('success', () => {
console.log('Job succeeded', job.results)
})
job.startWork(async (ctx) => {
const fileName = `${Math.random().toString(36).slice(2)}.wav`
await ctx
.beginBatch()
.run(`espeak "${req.body}" -w /golem/output/output.wav`)
.downloadFile('/golem/output/output.wav', `public/${fileName}`)
.end()
return fileName
})
res.send(`Job started! ID: ${job.id}`)
})
Getting the job state
Now let's add another endpoint that will allow us to get the state of the job. We can get the job by its ID with the getJobById()
method. Then we can simply return the state of the job to the user.
app.get('/tts/:id', async (req, res) => {
const job = golemClient.getJobById(req.params.id)
if (!job) {
res.status(404).send('Job not found')
return
}
res.send("Job's state is: " + job.state)
})
Getting the job results
Finally, let's add an endpoint that will allow us to get the results of the job. Here we will use the getJobById()
method again to get the job by its ID. In case the job is still running, we will wait for it to finish with the waitForResult()
method. Then we will get the results of the job with the results
property and return them to the user.
Let's also serve the files in the /public
directory so that the user can access them. We will use the express.static()
method for that.
// serve files in the /public directory
app.use('/results', express.static('public'))
app.get('/tts/:id/results', async (req, res) => {
const job = golemClient.getJobById(req.params.id)
if (!job) {
res.status(404).send('Job not found')
return
}
if (job.state !== JobState.Done) {
await job.waitForResult()
}
const results = await job.results
res.send(
`Job completed successfully! Open the following link in your browser to listen to the result: http://localhost:${port}/results/${results}`
)
})
Testing the app
We're done with the code! Let's start the app and test it.
First, let's start the server:
node index.mjs
Sending a POST request
Open a new terminal window. Let's send a POST request to the /tts
endpoint. Feel free to replace Hello Golem
with any text you want:
curl \
--header "Content-Type: text/plain" \
--request POST \
--data "Hello Golem" \
http://localhost:3000/tts
You should see the output:
Job started! ID: <job_id>
Make sure to write down the job ID, as we will need it in the next step.
Sending a GET request to get the job state
Now let's send a GET request to the /tts/<job_id>
endpoint to get the state of the job:
curl http://localhost:3000/tts/<job_id>
You should see the output:
pending
Wait a few seconds and send the same request again. You should see the output:
done
Sending a GET request to get the job results
Finally, let's send a GET request to the /tts/<job_id>/results
endpoint to get the results of the job:
curl http://localhost:3000/tts/<job_id>/results
You should see the output:
Job completed successfully! Open the following link in your browser to listen to the result: http://localhost:3000/results/<file_name>
Open the link in your browser, and you should hear the text you sent to the API!
Congratulations! You have just created a REST API that uses the Golem Network to process data! 🎉
Full code
Here's the full code of the index.mjs
file:
import express from 'express'
import { GolemNetwork, JobState } from '@golem-sdk/golem-js'
const app = express()
const port = 3000
app.use(express.text())
const golemClient = new GolemNetwork({
yagna: {
apiKey: 'try_golem',
},
})
await golemClient
.init()
.then(() => {
console.log('Connected to the Golem Network!')
})
.catch((error) => {
console.error('Failed to connect to the Golem Network:', error)
process.exit(1)
})
app.post('/tts', async (req, res) => {
if (!req.body) {
res.status(400).send('Missing text parameter')
return
}
const job = golemClient.createJob({
package: {
imageTag: 'severyn/espeak:latest',
},
})
job.events.on('created', () => {
console.log('Job created')
})
job.events.on('started', () => {
console.log('Job started')
})
job.events.on('error', () => {
console.log('Job failed', job.error)
})
job.events.on('success', () => {
console.log('Job succeeded', job.results)
})
job.startWork(async (ctx) => {
const fileName = `${Math.random().toString(36).slice(2)}.wav`
await ctx
.beginBatch()
.run(`espeak "${req.body}" -w /golem/output/output.wav`)
.downloadFile('/golem/output/output.wav', `public/${fileName}`)
.end()
return fileName
})
res.send(`Job started! ID: ${job.id}`)
})
app.get('/tts/:id', async (req, res) => {
const job = golemClient.getJobById(req.params.id)
if (!job) {
res.status(404).send('Job not found')
return
}
res.send("Job's state is: " + job.state)
})
// serve files in the /public directory
app.use('/results', express.static('public'))
app.get('/tts/:id/results', async (req, res) => {
const job = golemClient.getJobById(req.params.id)
if (!job) {
res.status(404).send('Job not found')
return
}
if (job.state !== JobState.Done) {
await job.waitForResult()
}
const results = await job.results
res.send(
`Job completed successfully! Open the following link in your browser to listen to the result: http://localhost:${port}/results/${results}`
)
})
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
process.on('SIGINT', async () => {
// cancel and cleanup all running jobs
await golemClient.close()
process.exit(0)
})