
Many of the applications I write for myself are small. In some way, Iāll need something done and I wonāt have any convenient way of doing it. For example, restarting a server I am using through service Y.
What I will do first is go through their web app and do it manually. This will generally be the last mile of the automation. I might have automation for alerting me that I need to restart a server, but I wonāt actually be able to restart it. Then, Iāll probably look to see if they have an API. If they do (awesome), then the API is fully featured and I can make use of it. Unfortunately, this can also make me feel overwhelmed. How can I implement this? Thereās way too many things I would have to incorporate.
So, This is the pattern I like to call āOne Trick Appsā. It has a minimal backend, a minimal frontend, and some kind of authentication between the two. They are generally not shared.
Overview
- The Frontend does an HTTP request to the Backend, with parameters and a unique authorization token.
- The Backend parses the request and makes an HTTP request to the API of the Service with a unique scoped token.
- The Backend gets a response, parses it and returns this response to the frontend.
- The Frontend gets a response, parses it and displays this response to the user.
An Example
I need to do something specific, so for this example, I will go with restarting a server from Linode.
First, I need to get an API token. If possible, Iāll scope it to the areas I need, like āLinodesā in this example. Since I will be issuing an update (or write), Iāll need Read/Write Access for that.

Great. Now, I can open this up in an API development app like Postman or Insomnia. There is a big field of apps in this space, For CLI you can try httpie or the absolute original: curl. Curl is the defacto app for nearly all testing and probably will be for a very long time.
With this, I can now view the API page. Knowing exactly what you want can be a little confusing. I definitely recommend searching around with keywords like āserviceā āintentā and āapiā to get an idea of what you need to use. In this case, weāre going to use Reboot a Linode.
Many API guides will also cover Authentication. You donāt want just anyone to be able to restart your server, right? So when making your requests, it will follow a structure of:
- HTTP Method (GET POST & Friends)
- Authentication (Bearer in this case)
- Url, such as /linode/instances/(linodeId)/reboot
The next part Iāll run into is understanding what I need past this. For example, what is a (linodeId)? If you go into the web app and click on your linode and look in the browser, you will often find a unique way to reference the page youāre off. This is probably your id.
Letās say our linode id is server932
and our personal token is 5fdd613b60ff
.
curl --request POST \--url https://api.linode.com/v4/linode/instances/server932/reboot \--header 'Accept: application/json' \--header 'Content-type: application/json' \--header 'Authorization: Bearer 5fdd613b60ff'
Submitting this will effectively restart your server. So beware. Once youāve confirmed itās working then the core piece:::: of your project is done.
Build the backend
Next, weāre going to build out the backend, stuff our token in it and create an endpoint for our client app to use. If you want to host your own server, thatās fine. Spin up a Node Express app, or web server in the language of your choice. There are multiple cloud services available that do this as well. However, they are generally in Javascript.
Weāre going to use val.town, a free / paid service for running tiny javascript endpoints. The real advantage here is weāll quickly have something publicly available. Otherwise, this will be identical for other hosting methods.
With a new script, weāll use Hono, a very minimalistic web server. This is the basic setup.
import { Hono } from 'hono'const app = new Hono()
app.get('/', (c) => c.text('Hono!'))
export default app
Letās shift things a bit for our purpose. Weāll need to do the following:
- Accept a query parameter (eg
?linode=server932
). - Verify it is our app, using a unique authentication token.
- Make a request to the Linode API.
- Return a response.
Weāre also not going to use any built in integration, so you can clearly see what is happening.
In the first part of the below code, we are gathering the Authorization header from the request.
Since the request will look like Bearer token
, we split it a and take the second value. If it
doesnāt match, we return status code 401.
Note that doing a return
r ends the request. Further code will not be processed.
Next, we take the query parameter server
. If it is missing s , we return a status code of
400, along with a short description of the issue.
Finally, we return a successful response. Weāre also using a format called jsend. This works well for general APIās like this one.
import { Hono } from "npm:hono";const app = new Hono();
app.get("/", c => { return c.text("Hello World");});
app.post("/server", c => { const authHeader = c.req.header("Authorization"); console.log({ authHeader }); if (!authHeader || authHeader.split(" ")[1] !== "5fdd613b60ff") { c.status(401); return c.json({ status: "failure", message: "Not Authorized", }); }
const server = c.req.query("server"); if (!server) { c.status(400); return c.json({ status: "failure", message: "Missing server query parameter", }); }
return c.json({ status: "success", message: `restarted server ${server}`, });});
export default app.fetch;
While this is functional and doesnāt do what we want yet, this is a great starter and covers everything the client will need to do to operate it. Weāll revisit the backend at the end, pulling it all together.
Build the frontend
Weāre going to write this in golang. It will accept a single argument, the server id. When you run it, it will contact our backend, which will restart the server for you. Because we wrote it with responses using the jsend format, it should be easy to parse.
We need to do a few things to be successful at this.
- Have a main function where everything is run.
- Collect information from the user (whatās the server id?).
- Make a network request with that information.
- Display the response to the user.
Hereās the code in full. Yes, compiling the code with the auth token t is bad. Instead, you can use a settings file, or define it each time with a flag. Weāll include this in a moment. Since we expect our response to always have a status and message, we can define r that type.
It is critical that we get the server id from the user. So we verify o that there is one argument provided
(os.Args[0]
is always the app name).
Thereās a lot of boilerplate for making a http request. There are shorter ways
of doing it, but this is very flexible. Effectively, we create a new client, set the query params q ,
set the headers and then tell it to ādoā the request (client.Do(req)
).
We want to make sure we close reading the body stream b . So we defer closing it. If there is an error, there wonāt be a body, it might be nil, so we check for that.
Assuming the response code is 200, we create a
ResponseBody
with the contents and p print out the message.
package main
import ( "encoding/json" "fmt" "io" "net/http" "os")
const url = "https://tyler71-restart-cloud-server-post.val.run/server"const authToken = "5fdd613b60ff"
type ResponseBody struct { Status string `json:"status"` Message string `json:"message"`}
func main() { if len(os.Args) != 2 { fmt.Println("Provide a server id, example ./restart_linode_server server955") return } server := os.Args[1]
req, err := http.NewRequest("POST", url, nil) if err != nil { fmt.Println("Error creating request:", err) return }
q := req.URL.Query() q.Add("server", server) req.URL.RawQuery = q.Encode()
req.Header = http.Header{ "Content-Type": {"application/json"}, "Authorization": {"Bearer " + authToken}, }
client := http.Client{} resp, err := client.Do(req) if err != nil { fmt.Println("Error sending request:", err) return } defer func(Body io.ReadCloser) {7 collapsed lines
if Body != nil { err = Body.Close() } if err != nil { fmt.Println("Error closing body", err) } }(req.Body)
body, err := io.ReadAll(resp.Body) if err != nil { fmt.Println("Error reading response body:", err) return }
if resp.StatusCode == http.StatusOK { var response ResponseBody err = json.Unmarshal(body, &response) fmt.Println(response.Message) return } else { fmt.Println(string(body)) return }}
Letās build it and run it.
go build -o restart_linode_server main.go./restart_linode_server server932restarted server server932
This completes the boilerplate. Our golang cli app contacts the server, the server does āthingsā and returns the response to us, which we display to the user.
Pulling It All Together
Letās make it functional. All we need to do is go into the server, call the right API and return the actual response. If you recall, hereās the query we needed to actually restart the server:
curl --request POST \--url https://api.linode.com/v4/linode/instances/server932/reboot \--header 'Accept: application/json' \--header 'Content-type: application/json' \--header 'Authorization: Bearer 5fdd613b60ff'
Letās convert this into an async TypeScript function.
const apiUrl = "https://api.linode.com/v4"
async function restartLinodeServer(authorization: string, serverId: string): Promise<boolean> { const headers = { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${authorization}`, } const res = await fetch(`${apiUrl}/linode/instances/${serverId}/reboot`) return res.ok}
Finally, we can drop this in and use it. Because we have the potential to fail, we also check for this when r returning our response.
import { Hono } from "npm:hono";const app = new Hono();
const apiToken = "450331c3"const apiUrl = "https://api.linode.com/v4"
async function restartLinodeServer(authorization: string, serverId: string): Promise<boolean> {7 collapsed lines
const headers = { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${authorization}`, } const res = await fetch(`${apiUrl}/linode/instances/${serverId}/reboot`) return res.ok}22 collapsed lines
app.get("/", c => { return c.text("Hello World");});
app.post("/server", async c => { const authHeader = c.req.header("Authorization"); console.log({ authHeader }); if (!authHeader || authHeader.split(" ")[1] !== "5fdd613b60ff") { c.status(401); return c.json({ status: "failure", message: "Not Authorized", }); }
const server = c.req.query("server"); if (!server) { c.status(400); return c.json({ status: "failure", message: "Missing server query parameter", }); }
const serverRestarted = await restartLinodeServer(apiToken, server)
if(serverRestarted) { return c.json({ status: "success", message: `restarted server ${server}`, }); } else { return c.json({ status: "failure", message: `failed to restart server ${server}`, }); }});
export default app.fetch;
Saving and running all of this.. promptly fails. The tokens are fake and the server idās are fake. So, thatās to be expected. But with tweaks to make it live, you may now speedily restart any linode servers in one command.
Iāll create a new post at some point (or update this one) with a simple skeleton for a backend / frontend to freely copy for use.