In this article, we are going to build a simple web server using Oak.
The web server will serve files, and perform CRUD operations. And we will use DenoDB as an ORM.
Requirements
- Deno installed
What is Oak?
Oak is a middleware framework for Denos native HTTP server, Deno Deploy, and Node.js 16.5 and later. It also includes a middleware route. This middleware framework is inspired by Koa.
Project structure
oak-demo/
file/
index.html
controllers/
Controllers.ts
model/
Person.ts
db.ts
main.ts
Hello World
main.ts
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
const router = new Router();
router.get("/", (ctx) => {
ctx.response.body = "Hello World"
});
const app = new Application();
app.use(router.routes());
app.use(router.allowedMethods());
app.listen({ port: 8080 });
Here, we import Application and Router classes and create a Router()
instance.
We use the get()
method and pass to it a path and a handler. The response body is a "Hello World" message.
Then we create an Application()
instance. And use the use()
method to register our middleware. Finally, we use the listen()
method, to start listening for requests.
Serving Files.
main.ts
import { Application, Router, send } from "https://deno.land/x/oak/mod.ts";
const ROOT_DIR = "./file";
const app = new Application();
app.use(async (ctx) => {
await send(ctx, "/index.html", {
root: ROOT_DIR,
});
});
app.use(router.routes());
app.use(router.allowedMethods());
app.listen({ port: 8080 });
In this snippet, we use the send()
function to serve a file from the local file system. We pass it the file's path and the root directory.
Deno requires read permission for the root directory.
Deno run --allow-net --allow-read main.ts
Deno DB
We create a model folder in our root directory and create a file called Person.ts
.
Person.ts
import { DataTypes, Model} from 'https://deno.land/x/denodb@v1.2.0/mod.ts';
export class Person extends Model {
static table = "person";
static timestamps = true;
static fields = {
id: { primaryKey: true, autoIncrement: true },
firstName: DataTypes.STRING,
lastName: DataTypes.STRING,
};
}
In Person.ts we need to import Datatypes and Model classes from DenoDB and then create a Person class, that inherits the properties and methods from the Model class.
We named our table "person" and we mark timestamps as true to show the time when a row is created and updated.
Then, we define the fields of our model and its data types, in this case: id
, firstName
, and lastName
.
After that, we create a file in our root directory where we going to connect the database. I going to use SQLite, but DenoDB has support for PostgreSQL, MySQL, MariaDB, and MongoDB.
db.ts
import { Database, SQLite3Connector } from 'https://deno.land/x/denodb@v1.2.0/mod.ts';
import {Person} from './model/Person.ts';
const connector = new SQLite3Connector({
filepath: "./database.sqlite",
});
export const db = new Database(connector);
db.link([Person])
export default db
We need to import Database and SQLite3Connector from Deno Db and Person class from the model module.
We create an immutable variable to store an instance of the SQLite connector and specify the path of the SQL file. Then, we create an immutable variable to store an instance of a database class, connect our database, and after that, we link it to the model.
Now we create a controllers folder in the root directory and create a file to define our controllers.
Controllers.ts
In this file we define our handlers to perform CRUD operations. We will define 5 handlers, one to perform POST requests, one to perform PUT requests, one to perform DELETE requests, and two to perform GET requests.
POST request
import { Context, Status } from "https://deno.land/x/oak@v11.1.0/mod.ts";
import {Person} from '../model/Person.ts';
export const CreatePerson = async (ctx: Context) => {
const person = await ctx.request.body({type:"json"}).value;
try {
const createdPerson = await Person.create(person);
ctx.response.body = createdPerson;
ctx.response.type = "json";
ctx.response.status = Status.Created;
} catch( _err) {
ctx.response.status = Status.InternalServerError;
}
};
Here we create a variable to store the request body and pass it as an argument to the create method, to create a record in our database.
If the operation is successful, it will send the status code 201
as a response and Internal Server Error
if it's not.
GET requests
export const AllPersons = async (ctx: Context) => {
try {
const person = await Person.all()
ctx.response.body = person;
ctx.response.type = "json";
ctx.response.status = Status.OK;
} catch(_err) {
ctx.response.status = Status.InternalServerError;
}
};
In AllPersons
handlers we retrieve all the records in the database using the all()
method and store them in the variable person
. The handler sends all the records and 200
status code as a response.
If there's an error, the handler will send an Internal Server Error
as a status code.
export const GetPerson = async (ctx: Context) => {
const id = ctx.params.id
try {
const person = await Person.where('id', id).first()
if (!person) {
ctx.response.status = Status.NotFound
} else {
ctx.response.body = person;
ctx.response.type = "json";
ctx.response.status = Status.OK;
}
} catch (_err) {
ctx.response.status = Status.InternalServerError;
}
}
Here, we extract the parameter id
from the path and store it in a variable. Then, we pass the id
as an argument to the where()
method, to retrieve the record that matches the ID passed and send it as a response with a 200
status code. If there is no record that matches the ID passed, the handler will send a Not Found
status code.
PUT requests
export const UpdatePerson = async (ctx: Context) => {
const id = ctx.params.id
const reqBody = await ctx.request.body().value;
try {
const person = await Person.where('id', id).first()
if (!person) {
ctx.response.status = Status.NotFound
} else {
await Person.where('id', id).update(reqBody)
ctx.response.body = "Person Updated";
ctx.response.type = "json";
ctx.response.status = Status.OK;
}
} catch (_err) {
ctx.response.status = Status.InternalServerError;
}
};
In UpdatePerson
handler, we extract the id
from the path and the request body. Then, we use the where()
method to look for a record that matches the ID. If the operation is successful, we pass the request body as an argument to the update()
method. After the record is modified, the handler sends "Person Updated" as a response body and 200
as a status code.
DELETE requests
export const DeletePerson = async (ctx: Context) => {
const id = ctx.params.id
try {
const person = await Person.where('id', id).first()
if (!person) {
ctx.response.status = Status.NotFound
} else {
Person.delete()
ctx.response.body = "Person deleted";
ctx.response.type = "json";
ctx.response.status = Status.OK;
}
} catch (_err) {
ctx.response.status = Status.InternalServerError;
}
};
Here we extract the id
from the path and retrieve a record that matches the ID, then we use the delete()
method to delete the record. After the record is deleted, the handler sends "Person deleted" as a response body, and 200
as a status code.
main.ts
import { Application,Router, send } from "https://deno.land/x/oak@v11.1.0/mod.ts";
import {db } from "./db.ts"
import {AllPersons, GetPerson, CreatePerson, UpdatePerson, DeletePerson} from "./controllers/Controllers.ts"
const ROOT_DIR = "./file";
const app = new Application();
const router = new Router();
router.get("/persons", AllPersons)
router.get("/person/:id", GetPerson);
router.post("/person", CreatePerson);
router.put("/person/:id", UpdatePerson);
router.delete("/person/:id", DeletePerson);
app.use(router.routes());
app.use(async (ctx) => {
await send(ctx, "/index.html", {
root: ROOT_DIR,
});
});
await db.sync()
app.listen({ port: 8080 });
In main.ts
we import all our handlers from Controller.ts
. Then we create a router instance and define our routes. For every HTTP method, we define a path and pass the handler it will call.
We use the use()
method to add our routes as middleware.
We use sync()
method to start Deno DB.
To run the server we write on our command line:
deno run --allow-all main.ts
Adding Logging
...
import logger from "https://deno.land/x/oak_logger/mod.ts";
...
app.use(logger.logger);
app.use(router.routes());
...
app.listen({ port: 8080 });
To add logging to our server, we import the module oak_logger
. And add logger.logger
as middleware on top of routes. Now, every time a request is made to our server, it will show the loggings in our command line. If we add the logger after app.use(router.routes());
, it will not show the loggings of the routes.
Conclusion
I found Oak easy to use, and its documentation helps a lot, it is well-written. I find it very similar to Gin even when they are written in different languages. Also, you can visit the awesome-oak page, it has a list of community projects for Oak.
Thank you for taking the time to read this article.
If you have any recommendations about other packages, architectures, how to improve my code, my English, or anything; please leave a comment or contact me through Twitter, or LinkedIn.
The source code is here.
Top comments (0)